Skip to content

feat(runtime-vapor): use shallow clone to support shallowRef in v-for #12985

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: minor
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions packages/runtime-vapor/__tests__/for.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -417,20 +417,20 @@ describe('createFor', () => {
'<li>0. 1</li><li>1. 2</li><li>2. 3</li><li>3. 4</li><!--for-->',
)

// change deep value should not update
// change
list.value[0].name = 'a'
setList()
await nextTick()
expect(host.innerHTML).toBe(
'<li>0. 1</li><li>1. 2</li><li>2. 3</li><li>3. 4</li><!--for-->',
'<li>0. a</li><li>1. 2</li><li>2. 3</li><li>3. 4</li><!--for-->',
)

Comment on lines 419 to 427
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feature was initially removed for performance optimization:
vuejs/vue-vapor#280
vuejs/vue-vapor@b962aa5

// remove
list.value.splice(1, 1)
setList()
await nextTick()
expect(host.innerHTML).toBe(
'<li>0. 1</li><li>1. 3</li><li>2. 4</li><!--for-->',
'<li>0. a</li><li>1. 3</li><li>2. 4</li><!--for-->',
)

// clear
Expand Down
46 changes: 37 additions & 9 deletions packages/runtime-vapor/src/apiCreateFor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,16 @@ import {
toReactive,
toReadonly,
} from '@vue/reactivity'
import { getSequence, isArray, isObject, isString } from '@vue/shared'
import {
extend,
getSequence,
isArray,
isMap,
isObject,
isSet,
isString,
looseEqual,
} from '@vue/shared'
import { createComment, createTextNode } from './dom/node'
import {
type Block,
Expand Down Expand Up @@ -84,6 +93,7 @@ export const createFor = (
}

let isMounted = false
let prevNeedsWrap: boolean
let oldBlocks: ForBlock[] = []
let newBlocks: ForBlock[]
let parent: ParentNode | undefined | null
Expand All @@ -99,7 +109,8 @@ export const createFor = (
}

const renderList = () => {
const source = normalizeSource(src())
const source = normalizeSource(src(), prevNeedsWrap)
prevNeedsWrap = source.needsWrap
const newLength = source.values.length
const oldLength = oldBlocks.length
newBlocks = new Array(newLength)
Expand Down Expand Up @@ -132,7 +143,7 @@ export const createFor = (
// unkeyed fast path
const commonLength = Math.min(newLength, oldLength)
for (let i = 0; i < commonLength; i++) {
update((newBlocks[i] = oldBlocks[i]), getItem(source, i)[0])
update(source, (newBlocks[i] = oldBlocks[i]), getItem(source, i)[0])
}
for (let i = oldLength; i < newLength; i++) {
mount(source, i)
Expand Down Expand Up @@ -249,6 +260,7 @@ export const createFor = (
moved = true
}
update(
source,
(newBlocks[newIndex] = prevBlock),
...getItem(source, newIndex),
)
Expand Down Expand Up @@ -336,22 +348,27 @@ export const createFor = (
return block
}

const tryPatchIndex = (source: any, idx: number) => {
const tryPatchIndex = (source: ResolvedSource, idx: number) => {
const block = oldBlocks[idx]
const [item, key, index] = getItem(source, idx)
if (block.key === getKey!(item, key, index)) {
update((newBlocks[idx] = block), item)
update(source, (newBlocks[idx] = block), item)
return true
}
}

const update = (
{ needsWrap }: ResolvedSource,
{ itemRef, keyRef, indexRef }: ForBlock,
newItem: any,
newKey?: any,
newIndex?: any,
) => {
if (newItem !== itemRef.value) {
if (
needsWrap
? newItem !== itemRef.value
: !looseEqual(newItem, itemRef.value)
) {
itemRef.value = newItem
}
if (keyRef && newKey !== undefined && newKey !== keyRef.value) {
Expand Down Expand Up @@ -393,9 +410,8 @@ export function createForSlots(
return slots
}

function normalizeSource(source: any): ResolvedSource {
function normalizeSource(source: any, needsWrap = false): ResolvedSource {
let values = source
let needsWrap = false
let isReadonlySource = false
let keys
if (isArray(source)) {
Expand Down Expand Up @@ -431,6 +447,18 @@ function normalizeSource(source: any): ResolvedSource {
}
}

function shallowClone(val: any) {
return Array.isArray(val)
? val.slice()
: isObject(val)
? extend({}, val)
: isMap(val)
? new Map(val)
: isSet(val)
? new Set(val)
: val
}

function getItem(
{ keys, values, needsWrap, isReadonlySource }: ResolvedSource,
idx: number,
Expand All @@ -439,7 +467,7 @@ function getItem(
? isReadonlySource
? toReadonly(toReactive(values[idx]))
: toReactive(values[idx])
: values[idx]
: shallowClone(values[idx])
if (keys) {
return [value, keys[idx], idx]
} else {
Expand Down