简单说下vue3
关于一些vue2与vue3的变化与使用区别在这份RFC(request fom comments)里面可以找到。
具体哪些变化不多说了, 网上vue3的文章有一大半是写这些的。
一段vue3的demo代码:

单从写法上来看,比2.x版本要引入的东西多了。感觉是一把梭哈与按需引用的区别。
所以对我们vue的使用者来说无非是这样:
- 用起来方便,但是build出来的代码size是最大的
- 用起来麻烦一点,但可以做到
tree shaking,减少文件大小(目前看我们项目的 vue API 使用情况,不会有啥变化,没鸟用…)
此次重点看下 reactive。
从测试用例来看 reactive
test will tell you anything.
一个好的工具/类库/框架,从测试用例上应该就可以看出它有哪些功能。所以建议在阅读质量比较高的项目源码时,可以先从测试用例开始。
以下代码在 packages/reactivity/tests/reactive.spec.ts 中可以找到
这里的测试用例大概分类了一下,从这些测试用例里面,慢慢去发现它有哪些API,并且是干嘛的。
第一部分对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']) })复制代理对象的测试、嵌套代理是否具有响应
测试代理对象会不会因为深度而不被代理```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: {
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) }) ```
动态増删对象/数组増删```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 = {}
observed.foo = rawexpect(observed.foo).not.toBe(raw)expect(isReactive(observed.foo)).toBe(true)
})
<br />对比2.x,不必使用 `$set` 与 `$delete`了,赞。4. 多次代理的测试<br />主要测试是否重复代理,通过结果可以看出来,内部有一个东西在维护这层关系<br />倒腾过来倒腾过去 看看是否符合预期```javascripttest('observing already observed value should return same Proxy', () => {const original = { foo: 1 }const observed = reactive(original)const observed2 = reactive(observed)expect(observed2).toBe(observed)})test('observing the same value multiple times should return same Proxy', () => {const original = { foo: 1 }const observed = reactive(original)const observed2 = reactive(original)expect(observed2).toBe(observed)})test('should not pollute original object with Proxies', () => {const original: any = { foo: 1 }const original2 = { bar: 2 }const observed = reactive(original)const observed2 = reactive(original2)observed.bar = observed2expect(observed.bar).toBe(observed2)expect(original.bar).toBe(original2)})
- 拆解还原```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
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)
})
<br />观察其返回值:<br />6. 不能被代理的测试```javascripttest('non-observable values', () => {const assertValue = (value: any) => {reactive(value)expect(`value cannot be made reactive: ${String(value)}`).toHaveBeenWarnedLast()}// numberassertValue(1)// stringassertValue('foo')// booleanassertValue(false)// nullassertValue(null)// undefinedassertValue(undefined)// symbolconst s = Symbol()assertValue(s)// built-ins should work and return same valueconst p = Promise.resolve()expect(reactive(p)).toBe(p) // 不可以响应 直接吐回源对象const r = new RegExp('')expect(reactive(r)).toBe(r)const d = new Date()expect(reactive(d)).toBe(d)})
- 这个属性不要给我代理
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) })
> 然后运行: `./node_modules/.bin/jest packages/reactivity/__tests__/reactive.spec.ts`> 单独跑这个文件,`only`只在这个文件中生效<a name="7102f05b"></a>### reactive实现原理从上面的测试用例可以想到,reactive方法内部应该是有2个集合去做映射。代理对象 -> 原始对象原始对象 -> 代理对象> 这里使用了两个WeakMap做关联rawToReactive、reactiveToRaw。> 这里回忆一下WeakMap的特性```javascript// 4个 weekmap做缓存映射const rawToReactive = new WeakMap<any, any>()const reactiveToRaw = new WeakMap<any, any>()const rawToReadonly = new WeakMap<any, any>()const readonlyToRaw = new WeakMap<any, any>()// 2个做标识的weekmap// 单独设置某个对象被代理时只能是只读的const readonlyValues = new WeakSet<any>()// 标记某个对象不被响应const nonReactiveValues = new WeakSet<any>()
reactive(target) 函数大概做的事情
- 判断 target 是否在 readonlyToRaw 存在, 存在说明 target 是一个只读的代理对象,直接返回 target
- 判断 target 是否在 readonlyValues 存在,存在的话调用 readonly(target)
- 调用 createReactiveObject. (reactive与readonly函数后面的逻辑一样的所以封装在了这里)
- 判断是否是
对象,不是的话,直接返回这个值,开发模式下会打印warn信息 - 判断 target 是否在 rawToReactive 存在,存在说明 target 是一个已经被代理过的对象了,返回
rawToReactive.get(target) - 判断 target 是否在 reactiveToRaw 存在,存在说明 target 是一个代理对象,直接返回 target
判断 target 是
否可观察,否则直接返回 targettypescript const canObserve = (value: any): boolean => { return ( !value._isVue && !value._isVNode && isObservableType(toRawType(value)) && !nonReactiveValues.has(value) ) }observed = new Proxy(target, handlers). 数组与对象的handlers处理是两个文件
typescript const handlers = collectionTypes.has(target.constructor) ? collectionHandlers : baseHandlers往 rawToReactive、reactiveToRaw 里面set
- 返回 observed.
大概分这10个步骤吧, 主要的逻辑还是在 handlers 里面处理的。
handlers 里面主要劫持了5个属性(mutableHandlers 是reactive中对象的处理方法):
export const mutableHandlers: ProxyHandler<object> = {get: createGetter(false),set,deleteProperty,has,ownKeys}
思考一下这样的场景:
const a = {b: {c: {d: {e: {// ... 一直嵌套}}}}}
a 里面的属性嵌套对象,一直嵌好几层(自己脑补这里a可能有个十万八千层)
这个时候如果去代理的话,是不是要自己递归下去?
假如这样的场景呢:
var a = {b: {c: {a}}}
a.b.c.a 死循环了.
|
|
|
V
- 第一次调用 reactive() 时,只会代理一层。
- 当
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
: res } } ```? // need to lazy access readonly and reactive here to avoid// circular dependency 关键在这里 你啥使用get的时候 我啥时候给你再去给代理readonly(res): reactive(res)
