Skip to content

Commit

Permalink
feat(reactivity): more efficient reactivity system (#5912)
Browse files Browse the repository at this point in the history
fix #311, fix #1811, fix #6018, fix #7160, fix #8714, fix #9149, fix #9419, fix #9464
  • Loading branch information
johnsoncodehk authored Oct 27, 2023
1 parent 1b43693 commit 4a25e25
Show file tree
Hide file tree
Showing 23 changed files with 810 additions and 542 deletions.
165 changes: 164 additions & 1 deletion packages/reactivity/__tests__/computed.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,7 +184,7 @@ describe('reactivity/computed', () => {
// mutate n
n.value++
// on the 2nd run, plusOne.value should have already updated.
expect(plusOneValues).toMatchObject([1, 2, 2])
expect(plusOneValues).toMatchObject([1, 2])
})

it('should warn if trying to set a readonly computed', () => {
Expand Down Expand Up @@ -288,4 +288,167 @@ describe('reactivity/computed', () => {
oldValue: 2
})
})

// https://github.com/vuejs/core/pull/5912#issuecomment-1497596875
it('should query deps dirty sequentially', () => {
const cSpy = vi.fn()

const a = ref<null | { v: number }>({
v: 1
})
const b = computed(() => {
return a.value
})
const c = computed(() => {
cSpy()
return b.value?.v
})
const d = computed(() => {
if (b.value) {
return c.value
}
return 0
})

d.value
a.value!.v = 2
a.value = null
d.value
expect(cSpy).toHaveBeenCalledTimes(1)
})

// https://github.com/vuejs/core/pull/5912#issuecomment-1738257692
it('chained computed dirty reallocation after querying dirty', () => {
let _msg: string | undefined

const items = ref<number[]>()
const isLoaded = computed(() => {
return !!items.value
})
const msg = computed(() => {
if (isLoaded.value) {
return 'The items are loaded'
} else {
return 'The items are not loaded'
}
})

effect(() => {
_msg = msg.value
})

items.value = [1, 2, 3]
items.value = [1, 2, 3]
items.value = undefined

expect(_msg).toBe('The items are not loaded')
})

it('chained computed dirty reallocation after trigger computed getter', () => {
let _msg: string | undefined

const items = ref<number[]>()
const isLoaded = computed(() => {
return !!items.value
})
const msg = computed(() => {
if (isLoaded.value) {
return 'The items are loaded'
} else {
return 'The items are not loaded'
}
})

_msg = msg.value
items.value = [1, 2, 3]
isLoaded.value // <- trigger computed getter
_msg = msg.value
items.value = undefined
_msg = msg.value

expect(_msg).toBe('The items are not loaded')
})

// https://github.com/vuejs/core/pull/5912#issuecomment-1739159832
it('deps order should be consistent with the last time get value', () => {
const cSpy = vi.fn()

const a = ref(0)
const b = computed(() => {
return a.value % 3 !== 0
})
const c = computed(() => {
cSpy()
if (a.value % 3 === 2) {
return 'expensive'
}
return 'cheap'
})
const d = computed(() => {
return a.value % 3 === 2
})
const e = computed(() => {
if (b.value) {
if (d.value) {
return 'Avoiding expensive calculation'
}
}
return c.value
})

e.value
a.value++
e.value

expect(e.effect.deps.length).toBe(3)
expect(e.effect.deps.indexOf((b as any).dep)).toBe(0)
expect(e.effect.deps.indexOf((d as any).dep)).toBe(1)
expect(e.effect.deps.indexOf((c as any).dep)).toBe(2)
expect(cSpy).toHaveBeenCalledTimes(2)

a.value++
e.value

expect(cSpy).toHaveBeenCalledTimes(2)
})

it('should trigger by the second computed that maybe dirty', () => {
const cSpy = vi.fn()

const src1 = ref(0)
const src2 = ref(0)
const c1 = computed(() => src1.value)
const c2 = computed(() => (src1.value % 2) + src2.value)
const c3 = computed(() => {
cSpy()
c1.value
c2.value
})

c3.value
src1.value = 2
c3.value
expect(cSpy).toHaveBeenCalledTimes(2)
src2.value = 1
c3.value
expect(cSpy).toHaveBeenCalledTimes(3)
})

it('should trigger the second effect', () => {
const fnSpy = vi.fn()
const v = ref(1)
const c = computed(() => v.value)

effect(() => {
c.value
})
effect(() => {
c.value
fnSpy()
})

expect(fnSpy).toBeCalledTimes(1)
v.value = 2
expect(fnSpy).toBeCalledTimes(2)
})
})
62 changes: 16 additions & 46 deletions packages/reactivity/__tests__/deferredComputed.spec.ts
Original file line number Diff line number Diff line change
@@ -1,57 +1,32 @@
import { computed, deferredComputed, effect, ref } from '../src'
import { computed, effect, ref } from '../src'

describe('deferred computed', () => {
const tick = Promise.resolve()

test('should only trigger once on multiple mutations', async () => {
test('should not trigger if value did not change', () => {
const src = ref(0)
const c = deferredComputed(() => src.value)
const c = computed(() => src.value % 2)
const spy = vi.fn()
effect(() => {
spy(c.value)
})
expect(spy).toHaveBeenCalledTimes(1)
src.value = 1
src.value = 2
src.value = 3
// not called yet
expect(spy).toHaveBeenCalledTimes(1)
await tick
// should only trigger once
expect(spy).toHaveBeenCalledTimes(2)
expect(spy).toHaveBeenCalledWith(c.value)
})

test('should not trigger if value did not change', async () => {
const src = ref(0)
const c = deferredComputed(() => src.value % 2)
const spy = vi.fn()
effect(() => {
spy(c.value)
})
expect(spy).toHaveBeenCalledTimes(1)
src.value = 1
src.value = 2

await tick
// should not trigger
expect(spy).toHaveBeenCalledTimes(1)

src.value = 3
src.value = 4
src.value = 5
await tick
// should trigger because latest value changes
expect(spy).toHaveBeenCalledTimes(2)
})

test('chained computed trigger', async () => {
test('chained computed trigger', () => {
const effectSpy = vi.fn()
const c1Spy = vi.fn()
const c2Spy = vi.fn()

const src = ref(0)
const c1 = deferredComputed(() => {
const c1 = computed(() => {
c1Spy()
return src.value % 2
})
Expand All @@ -69,19 +44,18 @@ describe('deferred computed', () => {
expect(effectSpy).toHaveBeenCalledTimes(1)

src.value = 1
await tick
expect(c1Spy).toHaveBeenCalledTimes(2)
expect(c2Spy).toHaveBeenCalledTimes(2)
expect(effectSpy).toHaveBeenCalledTimes(2)
})

test('chained computed avoid re-compute', async () => {
test('chained computed avoid re-compute', () => {
const effectSpy = vi.fn()
const c1Spy = vi.fn()
const c2Spy = vi.fn()

const src = ref(0)
const c1 = deferredComputed(() => {
const c1 = computed(() => {
c1Spy()
return src.value % 2
})
Expand All @@ -98,26 +72,24 @@ describe('deferred computed', () => {
src.value = 2
src.value = 4
src.value = 6
await tick
// c1 should re-compute once.
expect(c1Spy).toHaveBeenCalledTimes(2)
expect(c1Spy).toHaveBeenCalledTimes(4)
// c2 should not have to re-compute because c1 did not change.
expect(c2Spy).toHaveBeenCalledTimes(1)
// effect should not trigger because c2 did not change.
expect(effectSpy).toHaveBeenCalledTimes(1)
})

test('chained computed value invalidation', async () => {
test('chained computed value invalidation', () => {
const effectSpy = vi.fn()
const c1Spy = vi.fn()
const c2Spy = vi.fn()

const src = ref(0)
const c1 = deferredComputed(() => {
const c1 = computed(() => {
c1Spy()
return src.value % 2
})
const c2 = deferredComputed(() => {
const c2 = computed(() => {
c2Spy()
return c1.value + 1
})
Expand All @@ -139,17 +111,17 @@ describe('deferred computed', () => {
expect(c2Spy).toHaveBeenCalledTimes(2)
})

test('sync access of invalidated chained computed should not prevent final effect from running', async () => {
test('sync access of invalidated chained computed should not prevent final effect from running', () => {
const effectSpy = vi.fn()
const c1Spy = vi.fn()
const c2Spy = vi.fn()

const src = ref(0)
const c1 = deferredComputed(() => {
const c1 = computed(() => {
c1Spy()
return src.value % 2
})
const c2 = deferredComputed(() => {
const c2 = computed(() => {
c2Spy()
return c1.value + 1
})
Expand All @@ -162,14 +134,13 @@ describe('deferred computed', () => {
src.value = 1
// sync access c2
c2.value
await tick
expect(effectSpy).toHaveBeenCalledTimes(2)
})

test('should not compute if deactivated before scheduler is called', async () => {
test('should not compute if deactivated before scheduler is called', () => {
const c1Spy = vi.fn()
const src = ref(0)
const c1 = deferredComputed(() => {
const c1 = computed(() => {
c1Spy()
return src.value % 2
})
Expand All @@ -179,7 +150,6 @@ describe('deferred computed', () => {
c1.effect.stop()
// trigger
src.value++
await tick
expect(c1Spy).toHaveBeenCalledTimes(1)
})
})
Loading

0 comments on commit 4a25e25

Please sign in to comment.