wip: ref tests passing

This commit is contained in:
Evan You 2022-05-25 15:36:10 +08:00
parent e1e5a75540
commit ac85a4217e
7 changed files with 424 additions and 281 deletions

View File

@ -8,6 +8,7 @@ import {
isArray,
emptyObject,
remove,
hasChanged,
isServerRendering,
invokeWithErrorHandling
} from 'core/util'
@ -348,15 +349,6 @@ function doWatch(
}
}
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is#polyfill
function hasChanged(x: unknown, y: unknown): boolean {
if (x === y) {
return x !== 0 || 1 / x === 1 / (y as number)
} else {
return x !== x && y !== y
}
}
function queuePostRenderEffect(fn: Function) {
// TODO
}

View File

@ -17,6 +17,23 @@ export {
CustomRefFactory
} from './reactivity/ref'
export {
reactive,
// readonly,
isReactive,
isReadonly,
isShallow,
// isProxy,
// shallowReactive,
// shallowReadonly,
// markRaw,
// toRaw,
ReactiveFlags,
// DeepReadonly,
// ShallowReactive,
UnwrapNestedRefs
} from './reactivity/reactive'
export {
watch,
watchEffect,

View File

@ -1,12 +1,44 @@
import { observe, Observer } from 'core/observer'
import { Ref, UnwrapRefSimple } from './ref'
export const enum ReactiveFlags {
SKIP = '__v_skip',
IS_READONLY = '__v_isReadonly',
IS_SHALLOW = '__v_isShallow',
RAW = '__v_raw'
}
export interface Target {
__ob__?: Observer
[ReactiveFlags.SKIP]?: boolean
[ReactiveFlags.IS_READONLY]?: boolean
[ReactiveFlags.IS_SHALLOW]?: boolean
[ReactiveFlags.RAW]?: any
}
export declare const ShallowReactiveMarker: unique symbol
export function reactive() {}
// only unwrap nested ref
export type UnwrapNestedRefs<T> = T extends Ref ? T : UnwrapRefSimple<T>
export function reactive<T extends object>(target: T): UnwrapNestedRefs<T>
export function reactive(target: object) {
// if trying to observe a readonly proxy, return the readonly version.
if (!isReadonly(target)) {
observe(target)
}
return target
}
export function isReactive(value: unknown): boolean {
return !!(value && (value as any).__ob__)
return !!(value && (value as Target).__ob__)
}
export function isShallow(value: unknown): boolean {
// TODO
return !!(value && (value as any).__ob__)
return !!(value && (value as Target).__v_isShallow)
}
export function isReadonly(value: unknown): boolean {
// TODO
return !!(value && (value as Target).__v_isReadonly)
}

View File

@ -1,6 +1,8 @@
import { defineReactive } from 'core/observer/index'
import type { ShallowReactiveMarker } from './reactive'
import { isReactive, ShallowReactiveMarker } from './reactive'
import type { IfAny } from 'typescript/utils'
import Dep from 'core/observer/dep'
import { warn, isArray } from 'core/util'
declare const RefSymbol: unique symbol
export declare const RawSymbol: unique symbol
@ -13,6 +15,10 @@ export interface Ref<T = any> {
* autocomplete, so we use a private Symbol instead.
*/
[RefSymbol]: true
/**
* @private
*/
dep: Dep
}
export function isRef<T>(r: Ref<T> | unknown): r is Ref<T>
@ -46,13 +52,13 @@ function createRef(rawValue: unknown, shallow: boolean) {
if (isRef(rawValue)) {
return rawValue
}
const ref = { __v_isRef: true }
defineReactive(ref, 'value', rawValue, null, shallow)
const ref: any = { __v_isRef: true, __v_isShallow: shallow }
ref.dep = defineReactive(ref, 'value', rawValue, null, shallow)
return ref
}
export function triggerRef(ref: Ref) {
// TODO triggerRefValue(ref, __DEV__ ? ref.value : void 0)
ref.dep.notify()
}
export function unref<T>(ref: T | Ref<T>): T {
@ -67,22 +73,93 @@ export type CustomRefFactory<T> = (
set: (value: T) => void
}
export function customRef() {
// TODO
class CustomRefImpl<T> {
public dep?: Dep = undefined
private readonly _get: ReturnType<CustomRefFactory<T>>['get']
private readonly _set: ReturnType<CustomRefFactory<T>>['set']
public readonly __v_isRef = true
constructor(factory: CustomRefFactory<T>) {
const dep = new Dep()
const { get, set } = factory(
() => dep.depend(),
() => dep.notify()
)
this._get = get
this._set = set
}
get value() {
return this._get()
}
set value(newVal) {
this._set(newVal)
}
}
export function customRef<T>(factory: CustomRefFactory<T>): Ref<T> {
return new CustomRefImpl(factory) as any
}
export type ToRefs<T = any> = {
[K in keyof T]: ToRef<T[K]>
}
export function toRefs() {
// TODO
export function toRefs<T extends object>(object: T): ToRefs<T> {
if (__DEV__ && !isReactive(object)) {
warn(`toRefs() expects a reactive object but received a plain one.`)
}
const ret: any = isArray(object) ? new Array(object.length) : {}
for (const key in object) {
ret[key] = toRef(object, key)
}
return ret
}
class ObjectRefImpl<T extends object, K extends keyof T> {
public readonly __v_isRef = true
constructor(
private readonly _object: T,
private readonly _key: K,
private readonly _defaultValue?: T[K]
) {}
get value() {
const val = this._object[this._key]
return val === undefined ? (this._defaultValue as T[K]) : val
}
set value(newVal) {
this._object[this._key] = newVal
}
}
export type ToRef<T> = IfAny<T, Ref<T>, [T] extends [Ref] ? T : Ref<T>>
export function toRef() {
// TODO
export function toRef<T extends object, K extends keyof T>(
object: T,
key: K
): ToRef<T[K]>
export function toRef<T extends object, K extends keyof T>(
object: T,
key: K,
defaultValue: T[K]
): ToRef<Exclude<T[K], undefined>>
export function toRef<T extends object, K extends keyof T>(
object: T,
key: K,
defaultValue?: T[K]
): ToRef<T[K]> {
const val = object[key]
return isRef(val)
? val
: (new ObjectRefImpl(object, key, defaultValue) as any)
}
/**

View File

@ -12,8 +12,10 @@ import {
isPrimitive,
isUndef,
isValidArrayIndex,
isServerRendering
isServerRendering,
hasChanged
} from '../util/index'
import { isRef } from '../../composition-api'
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
@ -107,7 +109,7 @@ function copyAugment(target: Object, src: Object, keys: Array<string>) {
* or the existing observer if the value already has one.
*/
export function observe(value: any, asRootData?: boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) {
if (!isObject(value) || isRef(value) || value instanceof VNode) {
return
}
let ob: Observer | void
@ -167,22 +169,23 @@ export function defineReactive(
}
}
}
return value
return isRef(value) ? value.value : value
},
set: function reactiveSetter(newVal) {
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
if (!hasChanged(value, newVal)) {
return
}
/* eslint-enable no-self-compare */
if (__DEV__ && customSetter) {
customSetter()
}
// #7981: for accessor properties without setter
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else if (getter) {
// #7981: for accessor properties without setter
return
} else if (isRef(value) && !isRef(newVal)) {
value.value = newVal
} else {
val = newVal
}
@ -190,6 +193,8 @@ export function defineReactive(
dep.notify()
}
})
return dep
}
/**

View File

@ -349,3 +349,12 @@ export function once(fn: Function): Function {
}
}
}
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/is#polyfill
export function hasChanged(x: unknown, y: unknown): boolean {
if (x === y) {
return x === 0 && 1 / x !== 1 / (y as number)
} else {
return x === x && y === y
}
}

View File

@ -1,6 +1,16 @@
import { ref, shallowRef, unref } from 'vca/reactivity/ref'
import {
ref,
isRef,
shallowRef,
unref,
triggerRef,
toRef,
toRefs,
customRef,
Ref
} from 'vca/reactivity/ref'
import { ReactiveEffect } from 'vca/reactivity/effect'
import { isReactive } from 'vca/reactivity/reactive'
import { isReactive, isShallow, reactive } from 'vca/reactivity/reactive'
const effect = (fn: () => any) => new ReactiveEffect(fn)
@ -54,59 +64,60 @@ describe('reactivity/ref', () => {
expect(dummy).toBe(2)
})
// it('should work like a normal property when nested in a reactive object', () => {
// const a = ref(1)
// const obj = reactive({
// a,
// b: {
// c: a
// }
// })
it('should work like a normal property when nested in a reactive object', () => {
const a = ref(1)
const obj = reactive({
a,
b: {
c: a
}
})
// let dummy1: number
// let dummy2: number
let dummy1: number
let dummy2: number
// effect(() => {
// dummy1 = obj.a
// dummy2 = obj.b.c
// })
effect(() => {
dummy1 = obj.a
dummy2 = obj.b.c
})
// const assertDummiesEqualTo = (val: number) =>
// [dummy1, dummy2].forEach(dummy => expect(dummy).toBe(val))
const assertDummiesEqualTo = (val: number) =>
[dummy1, dummy2].forEach(dummy => expect(dummy).toBe(val))
// assertDummiesEqualTo(1)
// a.value++
// assertDummiesEqualTo(2)
// obj.a++
// assertDummiesEqualTo(3)
// obj.b.c++
// assertDummiesEqualTo(4)
// })
assertDummiesEqualTo(1)
a.value++
assertDummiesEqualTo(2)
obj.a++
assertDummiesEqualTo(3)
obj.b.c++
assertDummiesEqualTo(4)
})
// it('should unwrap nested ref in types', () => {
// const a = ref(0)
// const b = ref(a)
it('should unwrap nested ref in types', () => {
const a = ref(0)
const b = ref(a)
// expect(typeof (b.value + 1)).toBe('number')
// })
expect(typeof (b.value + 1)).toBe('number')
})
// it('should unwrap nested values in types', () => {
// const a = {
// b: ref(0)
// }
it('should unwrap nested values in types', () => {
const a = {
b: ref(0)
}
// const c = ref(a)
const c = ref(a)
// expect(typeof (c.value.b + 1)).toBe('number')
// })
expect(typeof (c.value.b + 1)).toBe('number')
})
// it('should NOT unwrap ref types nested inside arrays', () => {
// const arr = ref([1, ref(3)]).value
// expect(isRef(arr[0])).toBe(false)
// expect(isRef(arr[1])).toBe(true)
// expect((arr[1] as Ref).value).toBe(3)
// })
it('should NOT unwrap ref types nested inside arrays', () => {
const arr = ref([1, ref(3)]).value
expect(isRef(arr[0])).toBe(false)
expect(isRef(arr[1])).toBe(true)
expect((arr[1] as Ref).value).toBe(3)
})
// Vue 2 does not observe array properties
// it('should unwrap ref types as props of arrays', () => {
// const arr = [ref(0)]
// const symbolKey = Symbol('')
@ -120,69 +131,69 @@ describe('reactivity/ref', () => {
// expect(arrRef[symbolKey as any]).toBe(2)
// })
// it('should keep tuple types', () => {
// const tuple: [number, string, { a: number }, () => number, Ref<number>] = [
// 0,
// '1',
// { a: 1 },
// () => 0,
// ref(0)
// ]
// const tupleRef = ref(tuple)
it('should keep tuple types', () => {
const tuple: [number, string, { a: number }, () => number, Ref<number>] = [
0,
'1',
{ a: 1 },
() => 0,
ref(0)
]
const tupleRef = ref(tuple)
// tupleRef.value[0]++
// expect(tupleRef.value[0]).toBe(1)
// tupleRef.value[1] += '1'
// expect(tupleRef.value[1]).toBe('11')
// tupleRef.value[2].a++
// expect(tupleRef.value[2].a).toBe(2)
// expect(tupleRef.value[3]()).toBe(0)
// tupleRef.value[4].value++
// expect(tupleRef.value[4].value).toBe(1)
// })
tupleRef.value[0]++
expect(tupleRef.value[0]).toBe(1)
tupleRef.value[1] += '1'
expect(tupleRef.value[1]).toBe('11')
tupleRef.value[2].a++
expect(tupleRef.value[2].a).toBe(2)
expect(tupleRef.value[3]()).toBe(0)
tupleRef.value[4].value++
expect(tupleRef.value[4].value).toBe(1)
})
// it('should keep symbols', () => {
// const customSymbol = Symbol()
// const obj = {
// [Symbol.asyncIterator]: ref(1),
// [Symbol.hasInstance]: { a: ref('a') },
// [Symbol.isConcatSpreadable]: { b: ref(true) },
// [Symbol.iterator]: [ref(1)],
// [Symbol.match]: new Set<Ref<number>>(),
// [Symbol.matchAll]: new Map<number, Ref<string>>(),
// [Symbol.replace]: { arr: [ref('a')] },
// [Symbol.search]: { set: new Set<Ref<number>>() },
// [Symbol.species]: { map: new Map<number, Ref<string>>() },
// [Symbol.split]: new WeakSet<Ref<boolean>>(),
// [Symbol.toPrimitive]: new WeakMap<Ref<boolean>, string>(),
// [Symbol.toStringTag]: { weakSet: new WeakSet<Ref<boolean>>() },
// [Symbol.unscopables]: { weakMap: new WeakMap<Ref<boolean>, string>() },
// [customSymbol]: { arr: [ref(1)] }
// }
it('should keep symbols', () => {
const customSymbol = Symbol()
const obj = {
[Symbol.asyncIterator]: ref(1),
[Symbol.hasInstance]: { a: ref('a') },
[Symbol.isConcatSpreadable]: { b: ref(true) },
[Symbol.iterator]: [ref(1)],
[Symbol.match]: new Set<Ref<number>>(),
[Symbol.matchAll]: new Map<number, Ref<string>>(),
[Symbol.replace]: { arr: [ref('a')] },
[Symbol.search]: { set: new Set<Ref<number>>() },
[Symbol.species]: { map: new Map<number, Ref<string>>() },
[Symbol.split]: new WeakSet<Ref<boolean>>(),
[Symbol.toPrimitive]: new WeakMap<Ref<boolean>, string>(),
[Symbol.toStringTag]: { weakSet: new WeakSet<Ref<boolean>>() },
[Symbol.unscopables]: { weakMap: new WeakMap<Ref<boolean>, string>() },
[customSymbol]: { arr: [ref(1)] }
}
// const objRef = ref(obj)
const objRef = ref(obj)
// const keys: (keyof typeof obj)[] = [
// Symbol.asyncIterator,
// Symbol.hasInstance,
// Symbol.isConcatSpreadable,
// Symbol.iterator,
// Symbol.match,
// Symbol.matchAll,
// Symbol.replace,
// Symbol.search,
// Symbol.species,
// Symbol.split,
// Symbol.toPrimitive,
// Symbol.toStringTag,
// Symbol.unscopables,
// customSymbol
// ]
const keys: (keyof typeof obj)[] = [
Symbol.asyncIterator,
Symbol.hasInstance,
Symbol.isConcatSpreadable,
Symbol.iterator,
Symbol.match,
Symbol.matchAll,
Symbol.replace,
Symbol.search,
Symbol.species,
Symbol.split,
Symbol.toPrimitive,
Symbol.toStringTag,
Symbol.unscopables,
customSymbol
]
// keys.forEach(key => {
// expect(objRef.value[key]).toStrictEqual(obj[key])
// })
// })
keys.forEach(key => {
expect(objRef.value[key]).toStrictEqual(obj[key])
})
})
test('unref', () => {
expect(unref(1)).toBe(1)
@ -204,192 +215,192 @@ describe('reactivity/ref', () => {
expect(dummy).toBe(2)
})
// test('shallowRef force trigger', () => {
// const sref = shallowRef({ a: 1 })
// let dummy
// effect(() => {
// dummy = sref.value.a
// })
// expect(dummy).toBe(1)
test('shallowRef force trigger', () => {
const sref = shallowRef({ a: 1 })
let dummy
effect(() => {
dummy = sref.value.a
})
expect(dummy).toBe(1)
// sref.value.a = 2
// expect(dummy).toBe(1) // should not trigger yet
sref.value.a = 2
expect(dummy).toBe(1) // should not trigger yet
// // force trigger
// triggerRef(sref)
// expect(dummy).toBe(2)
// })
// force trigger
triggerRef(sref)
expect(dummy).toBe(2)
})
// test('shallowRef isShallow', () => {
// expect(isShallow(shallowRef({ a: 1 }))).toBe(true)
// })
test('shallowRef isShallow', () => {
expect(isShallow(shallowRef({ a: 1 }))).toBe(true)
})
// test('isRef', () => {
// expect(isRef(ref(1))).toBe(true)
// expect(isRef(computed(() => 1))).toBe(true)
test('isRef', () => {
expect(isRef(ref(1))).toBe(true)
// TODO expect(isRef(computed(() => 1))).toBe(true)
// expect(isRef(0)).toBe(false)
// expect(isRef(1)).toBe(false)
// // an object that looks like a ref isn't necessarily a ref
// expect(isRef({ value: 0 })).toBe(false)
// })
expect(isRef(0)).toBe(false)
expect(isRef(1)).toBe(false)
// an object that looks like a ref isn't necessarily a ref
expect(isRef({ value: 0 })).toBe(false)
})
// test('toRef', () => {
// const a = reactive({
// x: 1
// })
// const x = toRef(a, 'x')
// expect(isRef(x)).toBe(true)
// expect(x.value).toBe(1)
test('toRef', () => {
const a = reactive({
x: 1
})
const x = toRef(a, 'x')
expect(isRef(x)).toBe(true)
expect(x.value).toBe(1)
// // source -> proxy
// a.x = 2
// expect(x.value).toBe(2)
// source -> proxy
a.x = 2
expect(x.value).toBe(2)
// // proxy -> source
// x.value = 3
// expect(a.x).toBe(3)
// proxy -> source
x.value = 3
expect(a.x).toBe(3)
// // reactivity
// let dummyX
// effect(() => {
// dummyX = x.value
// })
// expect(dummyX).toBe(x.value)
// reactivity
let dummyX
effect(() => {
dummyX = x.value
})
expect(dummyX).toBe(x.value)
// // mutating source should trigger effect using the proxy refs
// a.x = 4
// expect(dummyX).toBe(4)
// mutating source should trigger effect using the proxy refs
a.x = 4
expect(dummyX).toBe(4)
// // should keep ref
// const r = { x: ref(1) }
// expect(toRef(r, 'x')).toBe(r.x)
// })
// should keep ref
const r = { x: ref(1) }
expect(toRef(r, 'x')).toBe(r.x)
})
// test('toRef default value', () => {
// const a: { x: number | undefined } = { x: undefined }
// const x = toRef(a, 'x', 1)
// expect(x.value).toBe(1)
test('toRef default value', () => {
const a: { x: number | undefined } = { x: undefined }
const x = toRef(a, 'x', 1)
expect(x.value).toBe(1)
// a.x = 2
// expect(x.value).toBe(2)
a.x = 2
expect(x.value).toBe(2)
// a.x = undefined
// expect(x.value).toBe(1)
// })
a.x = undefined
expect(x.value).toBe(1)
})
// test('toRefs', () => {
// const a = reactive({
// x: 1,
// y: 2
// })
test('toRefs', () => {
const a = reactive({
x: 1,
y: 2
})
// const { x, y } = toRefs(a)
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)
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)
// 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)
// proxy -> source
x.value = 3
y.value = 4
expect(a.x).toBe(3)
expect(a.y).toBe(4)
// // reactivity
// let dummyX, dummyY
// effect(() => {
// dummyX = x.value
// dummyY = y.value
// })
// expect(dummyX).toBe(x.value)
// expect(dummyY).toBe(y.value)
// reactivity
let dummyX, dummyY
effect(() => {
dummyX = x.value
dummyY = y.value
})
expect(dummyX).toBe(x.value)
expect(dummyY).toBe(y.value)
// // mutating source should trigger effect using the proxy refs
// a.x = 4
// a.y = 5
// expect(dummyX).toBe(4)
// expect(dummyY).toBe(5)
// })
// mutating source should trigger effect using the proxy refs
a.x = 4
a.y = 5
expect(dummyX).toBe(4)
expect(dummyY).toBe(5)
})
// test('toRefs should warn on plain object', () => {
// toRefs({})
// expect(`toRefs() expects a reactive object`).toHaveBeenWarned()
// })
test('toRefs should warn on plain object', () => {
toRefs({})
expect(`toRefs() expects a reactive object`).toHaveBeenWarned()
})
// test('toRefs should warn on plain array', () => {
// toRefs([])
// expect(`toRefs() expects a reactive object`).toHaveBeenWarned()
// })
test('toRefs should warn on plain array', () => {
toRefs([])
expect(`toRefs() expects a reactive object`).toHaveBeenWarned()
})
// test('toRefs reactive array', () => {
// const arr = reactive(['a', 'b', 'c'])
// const refs = toRefs(arr)
test('toRefs reactive array', () => {
const arr = reactive(['a', 'b', 'c'])
const refs = toRefs(arr)
// expect(Array.isArray(refs)).toBe(true)
expect(Array.isArray(refs)).toBe(true)
// refs[0].value = '1'
// expect(arr[0]).toBe('1')
refs[0].value = '1'
expect(arr[0]).toBe('1')
// arr[1] = '2'
// expect(refs[1].value).toBe('2')
// })
arr[1] = '2'
expect(refs[1].value).toBe('2')
})
// test('customRef', () => {
// let value = 1
// let _trigger: () => void
test('customRef', () => {
let value = 1
let _trigger: () => void
// const custom = customRef((track, trigger) => ({
// get() {
// track()
// return value
// },
// set(newValue: number) {
// value = newValue
// _trigger = trigger
// }
// }))
const custom = customRef((track, trigger) => ({
get() {
track()
return value
},
set(newValue: number) {
value = newValue
_trigger = trigger
}
}))
// expect(isRef(custom)).toBe(true)
expect(isRef(custom)).toBe(true)
// let dummy
// effect(() => {
// dummy = custom.value
// })
// expect(dummy).toBe(1)
let dummy
effect(() => {
dummy = custom.value
})
expect(dummy).toBe(1)
// custom.value = 2
// // should not trigger yet
// expect(dummy).toBe(1)
custom.value = 2
// should not trigger yet
expect(dummy).toBe(1)
// _trigger!()
// expect(dummy).toBe(2)
// })
_trigger!()
expect(dummy).toBe(2)
})
// test('should not trigger when setting value to same proxy', () => {
// const obj = reactive({ count: 0 })
test('should not trigger when setting value to same proxy', () => {
const obj = reactive({ count: 0 })
// const a = ref(obj)
// const spy1 = jest.fn(() => a.value)
const a = ref(obj)
const spy1 = vi.fn(() => a.value)
// effect(spy1)
effect(spy1)
// a.value = obj
// expect(spy1).toBeCalledTimes(1)
a.value = obj
expect(spy1).toBeCalledTimes(1)
// const b = shallowRef(obj)
// const spy2 = jest.fn(() => b.value)
const b = shallowRef(obj)
const spy2 = vi.fn(() => b.value)
// effect(spy2)
effect(spy2)
// b.value = obj
// expect(spy2).toBeCalledTimes(1)
// })
b.value = obj
expect(spy2).toBeCalledTimes(1)
})
})