Skip to content

Commit d99b91d

Browse files
pikaxantfu
andauthored
fix(reactive): fix issue when using reactive array in the template (#532)
Co-authored-by: Anthony Fu <[email protected]>
1 parent 4d39443 commit d99b91d

File tree

4 files changed

+192
-6
lines changed

4 files changed

+192
-6
lines changed

README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,33 @@ b.list[0].count.value === 0 // true
152152

153153
</details>
154154

155+
<details>
156+
<summary>
157+
✅ <b>Should</b> always use <code>ref</code> in a <code>reactive</code> when working with <code>Array</code>
158+
</summary>
159+
160+
```js
161+
const a = reactive({
162+
list: [
163+
reactive({
164+
count: ref(0),
165+
}),
166+
]
167+
})
168+
// unwrapped
169+
a.list[0].count === 0 // true
170+
171+
a.list.push(
172+
reactive({
173+
count: ref(1),
174+
})
175+
)
176+
// unwrapped
177+
a.list[1].count === 1 // true
178+
```
179+
180+
</details>
181+
155182
<details>
156183
<summary>
157184
⚠️ `set` workaround for adding new reactive properties

src/mixin.ts

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
SetupFunction,
66
Data,
77
} from './component'
8-
import { isRef, isReactive, toRefs } from './reactivity'
8+
import { isRef, isReactive, toRefs, isRaw } from './reactivity'
99
import {
1010
isPlainObject,
1111
assert,
@@ -14,6 +14,7 @@ import {
1414
isFunction,
1515
isObject,
1616
def,
17+
isArray,
1718
} from './utils'
1819
import { ref } from './apis'
1920
import vmStateManager from './utils/vmStateManager'
@@ -23,6 +24,7 @@ import {
2324
resolveScopedSlots,
2425
asVmProperty,
2526
} from './utils/instance'
27+
import { getVueConstructor } from './runtimeContext'
2628
import { createObserver } from './reactivity/reactive'
2729

2830
export function mixin(Vue: VueConstructor) {
@@ -121,7 +123,13 @@ export function mixin(Vue: VueConstructor) {
121123
bindingValue = bindingValue.bind(vm)
122124
} else if (!isObject(bindingValue)) {
123125
bindingValue = ref(bindingValue)
126+
} else if (hasReactiveArrayChild(bindingValue)) {
127+
// creates a custom reactive properties without make the object explicitly reactive
128+
// NOTE we should try to avoid this, better implementation needed
129+
customReactive(bindingValue)
124130
}
131+
} else if (isArray(bindingValue)) {
132+
bindingValue = ref(bindingValue)
125133
}
126134
}
127135
asVmProperty(vm, name, bindingValue)
@@ -140,6 +148,45 @@ export function mixin(Vue: VueConstructor) {
140148
}
141149
}
142150

151+
function customReactive(target: object) {
152+
if (
153+
!isPlainObject(target) ||
154+
isRef(target) ||
155+
isReactive(target) ||
156+
isRaw(target)
157+
)
158+
return
159+
const Vue = getVueConstructor()
160+
const defineReactive = Vue.util.defineReactive
161+
162+
Object.keys(target).forEach((k) => {
163+
const val = target[k]
164+
defineReactive(target, k, val)
165+
if (val) {
166+
customReactive(val)
167+
}
168+
return
169+
})
170+
}
171+
172+
function hasReactiveArrayChild(target: object, visited = new Map()): boolean {
173+
if (visited.has(target)) {
174+
return visited.get(target)
175+
}
176+
visited.set(target, false)
177+
if (Array.isArray(target) && isReactive(target)) {
178+
visited.set(target, true)
179+
return true
180+
}
181+
182+
if (!isPlainObject(target) || isRaw(target)) {
183+
return false
184+
}
185+
return Object.keys(target).some((x) =>
186+
hasReactiveArrayChild(target[x], visited)
187+
)
188+
}
189+
143190
function createSetupContext(
144191
vm: ComponentInstance & { [x: string]: any }
145192
): SetupContext {

src/reactivity/reactive.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { AnyObject } from '../types/basic'
22
import { getRegisteredVueOrDefault } from '../runtimeContext'
3-
import { isPlainObject, def, warn, hasOwn } from '../utils'
3+
import { isPlainObject, def, warn, isArray, hasOwn } from '../utils'
44
import { isComponentInstance, defineComponentInstance } from '../utils/helper'
55
import { RefKey } from '../utils/symbols'
66
import { isRef, UnwrapRef } from './ref'
@@ -130,7 +130,11 @@ export function shallowReactive(obj: any): any {
130130
return
131131
}
132132

133-
if (!isPlainObject(obj) || isRaw(obj) || !Object.isExtensible(obj)) {
133+
if (
134+
!(isPlainObject(obj) || isArray(obj)) ||
135+
isRaw(obj) ||
136+
!Object.isExtensible(obj)
137+
) {
134138
return obj as any
135139
}
136140

@@ -190,7 +194,11 @@ export function reactive<T extends object>(obj: T): UnwrapRef<T> {
190194
return
191195
}
192196

193-
if (!isPlainObject(obj) || isRaw(obj) || !Object.isExtensible(obj)) {
197+
if (
198+
!(isPlainObject(obj) || isArray(obj)) ||
199+
isRaw(obj) ||
200+
!Object.isExtensible(obj)
201+
) {
194202
return obj as any
195203
}
196204

@@ -201,7 +209,7 @@ export function reactive<T extends object>(obj: T): UnwrapRef<T> {
201209

202210
export function shallowReadonly<T extends object>(obj: T): Readonly<T>
203211
export function shallowReadonly(obj: any): any {
204-
if (!isPlainObject(obj) || !Object.isExtensible(obj)) {
212+
if (!(isPlainObject(obj) || isArray(obj)) || !Object.isExtensible(obj)) {
205213
return obj
206214
}
207215

@@ -254,7 +262,7 @@ export function shallowReadonly(obj: any): any {
254262
* Make sure obj can't be a reactive
255263
*/
256264
export function markRaw<T extends object>(obj: T): T {
257-
if (!isPlainObject(obj) || !Object.isExtensible(obj)) {
265+
if (!(isPlainObject(obj) || isArray(obj)) || !Object.isExtensible(obj)) {
258266
return obj
259267
}
260268

test/setup.spec.js

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -896,6 +896,110 @@ describe('setup', () => {
896896
expect(vm.$el.textContent).toBe('2')
897897
})
898898

899+
// #524
900+
it('should work with reactive arrays.', async () => {
901+
const opts = {
902+
template: `<div>{{items.length}}</div>`,
903+
setup() {
904+
const items = reactive([])
905+
906+
setTimeout(() => {
907+
items.push(2)
908+
}, 1)
909+
910+
return {
911+
items,
912+
}
913+
},
914+
}
915+
const Constructor = Vue.extend(opts).extend({})
916+
917+
const vm = new Vue(Constructor).$mount()
918+
expect(vm.$el.textContent).toBe('0')
919+
await sleep(10)
920+
await nextTick()
921+
expect(vm.$el.textContent).toBe('1')
922+
})
923+
924+
it('should work with reactive array nested', async () => {
925+
const opts = {
926+
template: `<div>{{a.items.length}}</div>`,
927+
setup() {
928+
const items = reactive([])
929+
930+
setTimeout(() => {
931+
items.push(2)
932+
}, 1)
933+
934+
return {
935+
a: {
936+
items,
937+
},
938+
}
939+
},
940+
}
941+
const Constructor = Vue.extend(opts).extend({})
942+
943+
const vm = new Vue(Constructor).$mount()
944+
expect(vm.$el.textContent).toBe('0')
945+
await sleep(10)
946+
await nextTick()
947+
expect(vm.$el.textContent).toBe('1')
948+
})
949+
950+
it('should not unwrap reactive array nested', async () => {
951+
const opts = {
952+
template: `<div>{{a.items}}</div>`,
953+
setup() {
954+
const items = reactive([])
955+
956+
setTimeout(() => {
957+
items.push(ref(1))
958+
}, 1)
959+
960+
return {
961+
a: {
962+
items,
963+
},
964+
}
965+
},
966+
}
967+
const Constructor = Vue.extend(opts).extend({})
968+
969+
const vm = new Vue(Constructor).$mount()
970+
expect(vm.$el.textContent).toBe('[]')
971+
await sleep(10)
972+
await nextTick()
973+
expect(JSON.parse(vm.$el.textContent)).toStrictEqual([{ value: 1 }])
974+
})
975+
976+
// TODO make this pass
977+
// it('should work with computed', async ()=>{
978+
// const opts = {
979+
// template: `<div>{{len}}</div>`,
980+
// setup() {
981+
// const array = reactive([]);
982+
// const len = computed(()=> array.length);
983+
984+
// setTimeout(() => {
985+
// array.push(2)
986+
// }, 1)
987+
988+
// return {
989+
// len
990+
// }
991+
// },
992+
// }
993+
// const Constructor = Vue.extend(opts).extend({})
994+
995+
// const vm = new Vue(Constructor).$mount()
996+
// expect(vm.$el.textContent).toBe('0')
997+
// await sleep(10)
998+
// await nextTick()
999+
// expect(vm.$el.textContent).toBe('1')
1000+
// })
1001+
1002+
8991003
// #448
9001004
it('should not cause infinite loop', async () => {
9011005
const A = defineComponent({

0 commit comments

Comments
 (0)