Skip to content

Commit f316a33

Browse files
unbyteyyx990803
andauthored
fix(reactivity): avoid length mutating array methods causing infinite updates (#2138)
fix #2137 Co-authored-by: Evan You <[email protected]>
1 parent 422f05e commit f316a33

File tree

2 files changed

+46
-3
lines changed

2 files changed

+46
-3
lines changed

packages/reactivity/__tests__/effect.spec.ts

+23
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,29 @@ describe('reactivity/effect', () => {
358358
expect(counterSpy).toHaveBeenCalledTimes(2)
359359
})
360360

361+
it('should avoid infinite recursive loops when use Array.prototype.push/unshift/pop/shift', () => {
362+
;(['push', 'unshift'] as const).forEach(key => {
363+
const arr = reactive<number[]>([])
364+
const counterSpy1 = jest.fn(() => (arr[key] as any)(1))
365+
const counterSpy2 = jest.fn(() => (arr[key] as any)(2))
366+
effect(counterSpy1)
367+
effect(counterSpy2)
368+
expect(arr.length).toBe(2)
369+
expect(counterSpy1).toHaveBeenCalledTimes(1)
370+
expect(counterSpy2).toHaveBeenCalledTimes(1)
371+
})
372+
;(['pop', 'shift'] as const).forEach(key => {
373+
const arr = reactive<number[]>([1, 2, 3, 4])
374+
const counterSpy1 = jest.fn(() => (arr[key] as any)())
375+
const counterSpy2 = jest.fn(() => (arr[key] as any)())
376+
effect(counterSpy1)
377+
effect(counterSpy2)
378+
expect(arr.length).toBe(2)
379+
expect(counterSpy1).toHaveBeenCalledTimes(1)
380+
expect(counterSpy2).toHaveBeenCalledTimes(1)
381+
})
382+
})
383+
361384
it('should allow explicitly recursive raw function loops', () => {
362385
const counter = reactive({ num: 0 })
363386
const numSpy = jest.fn(() => {

packages/reactivity/src/baseHandlers.ts

+23-3
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,13 @@ import {
88
reactiveMap
99
} from './reactive'
1010
import { TrackOpTypes, TriggerOpTypes } from './operations'
11-
import { track, trigger, ITERATE_KEY } from './effect'
11+
import {
12+
track,
13+
trigger,
14+
ITERATE_KEY,
15+
pauseTracking,
16+
enableTracking
17+
} from './effect'
1218
import {
1319
isObject,
1420
hasOwn,
@@ -32,22 +38,36 @@ const readonlyGet = /*#__PURE__*/ createGetter(true)
3238
const shallowReadonlyGet = /*#__PURE__*/ createGetter(true, true)
3339

3440
const arrayInstrumentations: Record<string, Function> = {}
41+
// instrument identity-sensitive Array methods to account for possible reactive
42+
// values
3543
;(['includes', 'indexOf', 'lastIndexOf'] as const).forEach(key => {
44+
const method = Array.prototype[key] as any
3645
arrayInstrumentations[key] = function(this: unknown[], ...args: unknown[]) {
3746
const arr = toRaw(this)
3847
for (let i = 0, l = this.length; i < l; i++) {
3948
track(arr, TrackOpTypes.GET, i + '')
4049
}
4150
// we run the method using the original args first (which may be reactive)
42-
const res = (arr[key] as any)(...args)
51+
const res = method.apply(arr, args)
4352
if (res === -1 || res === false) {
4453
// if that didn't work, run it again using raw values.
45-
return (arr[key] as any)(...args.map(toRaw))
54+
return method.apply(arr, args.map(toRaw))
4655
} else {
4756
return res
4857
}
4958
}
5059
})
60+
// instrument length-altering mutation methods to avoid length being tracked
61+
// which leads to infinite loops in some cases (#2137)
62+
;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => {
63+
const method = Array.prototype[key] as any
64+
arrayInstrumentations[key] = function(this: unknown[], ...args: unknown[]) {
65+
pauseTracking()
66+
const res = method.apply(this, args)
67+
enableTracking()
68+
return res
69+
}
70+
})
5171

5272
function createGetter(isReadonly = false, shallow = false) {
5373
return function get(target: Target, key: string | symbol, receiver: object) {

0 commit comments

Comments
 (0)