vue3-core/packages/runtime-vapor/src/apiCreateFor.ts

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

429 lines
12 KiB
TypeScript
Raw Normal View History

2025-01-30 22:36:41 +08:00
import {
EffectScope,
type ShallowRef,
isReactive,
isShallow,
shallowReadArray,
shallowRef,
toReactive,
} from '@vue/reactivity'
2025-01-29 19:07:40 +08:00
import { getSequence, isArray, isObject, isString } from '@vue/shared'
import { createComment, createTextNode } from './dom/node'
2025-02-03 15:46:40 +08:00
import {
type Block,
VaporFragment,
insert,
remove as removeBlock,
} from './block'
2025-01-29 19:07:40 +08:00
import { warn } from '@vue/runtime-dom'
import { currentInstance, isVaporComponent } from './component'
import type { DynamicSlot } from './componentSlots'
import { renderEffect } from './renderEffect'
2025-02-03 15:46:40 +08:00
class ForBlock extends VaporFragment {
2025-01-29 19:07:40 +08:00
scope: EffectScope | undefined
2024-12-11 11:50:17 +08:00
key: any
2025-01-29 19:07:40 +08:00
itemRef: ShallowRef<any>
keyRef: ShallowRef<any> | undefined
indexRef: ShallowRef<number | undefined> | undefined
2025-01-29 19:07:40 +08:00
constructor(
nodes: Block,
scope: EffectScope | undefined,
item: ShallowRef<any>,
key: ShallowRef<any> | undefined,
index: ShallowRef<number | undefined> | undefined,
renderKey: any,
2025-01-29 19:07:40 +08:00
) {
super(nodes)
this.scope = scope
this.itemRef = item
this.keyRef = key
this.indexRef = index
this.key = renderKey
2025-01-29 19:07:40 +08:00
}
2024-12-11 11:50:17 +08:00
}
type Source = any[] | Record<any, any> | number | Set<any> | Map<any, any>
2025-01-30 22:36:41 +08:00
type ResolvedSource = {
values: any[]
needsWrap: boolean
keys?: string[]
}
2025-01-29 19:07:40 +08:00
/*! #__NO_SIDE_EFFECTS__ */
2024-12-11 11:50:17 +08:00
export const createFor = (
src: () => Source,
renderItem: (
item: ShallowRef<any>,
key: ShallowRef<any>,
index: ShallowRef<number | undefined>,
) => Block,
2024-12-11 11:50:17 +08:00
getKey?: (item: any, key: any, index?: number) => any,
2025-01-29 19:07:40 +08:00
/**
* Whether this v-for is used directly on a component. If true, we can avoid
* creating an extra fragment / scope for each block
*/
isComponent = false,
2024-12-11 11:50:17 +08:00
once?: boolean,
2025-01-29 19:07:40 +08:00
// hydrationNode?: Node,
2025-02-03 15:46:40 +08:00
): VaporFragment => {
2025-01-29 19:07:40 +08:00
let isMounted = false
let oldBlocks: ForBlock[] = []
let newBlocks: ForBlock[]
let parent: ParentNode | undefined | null
const parentAnchor = __DEV__ ? createComment('for') : createTextNode()
const frag = new VaporFragment(oldBlocks)
2025-01-29 19:07:40 +08:00
const instance = currentInstance!
if (__DEV__ && !instance) {
warn('createFor() can only be used inside setup()')
}
const renderList = () => {
2025-01-30 22:36:41 +08:00
const source = normalizeSource(src())
const newLength = source.values.length
2025-01-29 19:07:40 +08:00
const oldLength = oldBlocks.length
newBlocks = new Array(newLength)
if (!isMounted) {
isMounted = true
for (let i = 0; i < newLength; i++) {
mount(source, i)
}
} else {
parent = parent || parentAnchor!.parentNode
if (!oldLength) {
// fast path for all new
for (let i = 0; i < newLength; i++) {
mount(source, i)
}
} else if (!newLength) {
// fast path for clearing
for (let i = 0; i < oldLength; i++) {
unmount(oldBlocks[i])
}
} else if (!getKey) {
// unkeyed fast path
const commonLength = Math.min(newLength, oldLength)
for (let i = 0; i < commonLength; i++) {
2025-01-30 22:36:41 +08:00
update((newBlocks[i] = oldBlocks[i]), getItem(source, i)[0])
2025-01-29 19:07:40 +08:00
}
for (let i = oldLength; i < newLength; i++) {
mount(source, i)
}
for (let i = newLength; i < oldLength; i++) {
unmount(oldBlocks[i])
}
} else {
let i = 0
let e1 = oldLength - 1 // prev ending index
let e2 = newLength - 1 // next ending index
// 1. sync from start
// (a b) c
// (a b) d e
while (i <= e1 && i <= e2) {
if (tryPatchIndex(source, i)) {
i++
} else {
break
}
}
// 2. sync from end
// a (b c)
// d e (b c)
while (i <= e1 && i <= e2) {
if (tryPatchIndex(source, i)) {
e1--
e2--
} else {
break
}
}
// 3. common sequence + mount
// (a b)
// (a b) c
// i = 2, e1 = 1, e2 = 2
// (a b)
// c (a b)
// i = 0, e1 = -1, e2 = 0
if (i > e1) {
if (i <= e2) {
const nextPos = e2 + 1
const anchor =
nextPos < newLength
? normalizeAnchor(newBlocks[nextPos].nodes)
: parentAnchor
while (i <= e2) {
mount(source, i, anchor)
i++
}
}
}
// 4. common sequence + unmount
// (a b) c
// (a b)
// i = 2, e1 = 2, e2 = 1
// a (b c)
// (b c)
// i = 0, e1 = 0, e2 = -1
else if (i > e2) {
while (i <= e1) {
unmount(oldBlocks[i])
i++
}
}
// 5. unknown sequence
// [i ... e1 + 1]: a b [c d e] f g
// [i ... e2 + 1]: a b [e d c h] f g
// i = 2, e1 = 4, e2 = 5
else {
const s1 = i // prev starting index
const s2 = i // next starting index
// 5.1 build key:index map for newChildren
const keyToNewIndexMap = new Map()
for (i = s2; i <= e2; i++) {
keyToNewIndexMap.set(getKey(...getItem(source, i)), i)
}
// 5.2 loop through old children left to be patched and try to patch
// matching nodes & remove nodes that are no longer present
let j
let patched = 0
const toBePatched = e2 - s2 + 1
let moved = false
// used to track whether any node has moved
let maxNewIndexSoFar = 0
// works as Map<newIndex, oldIndex>
// Note that oldIndex is offset by +1
// and oldIndex = 0 is a special value indicating the new node has
// no corresponding old node.
// used for determining longest stable subsequence
const newIndexToOldIndexMap = new Array(toBePatched).fill(0)
for (i = s1; i <= e1; i++) {
const prevBlock = oldBlocks[i]
if (patched >= toBePatched) {
// all new children have been patched so this can only be a removal
unmount(prevBlock)
} else {
const newIndex = keyToNewIndexMap.get(prevBlock.key)
if (newIndex == null) {
unmount(prevBlock)
} else {
newIndexToOldIndexMap[newIndex - s2] = i + 1
if (newIndex >= maxNewIndexSoFar) {
maxNewIndexSoFar = newIndex
} else {
moved = true
}
update(
(newBlocks[newIndex] = prevBlock),
...getItem(source, newIndex),
)
patched++
}
}
}
// 5.3 move and mount
// generate longest stable subsequence only when nodes have moved
const increasingNewIndexSequence = moved
? getSequence(newIndexToOldIndexMap)
: []
j = increasingNewIndexSequence.length - 1
// looping backwards so that we can use last patched node as anchor
for (i = toBePatched - 1; i >= 0; i--) {
const nextIndex = s2 + i
const anchor =
nextIndex + 1 < newLength
? normalizeAnchor(newBlocks[nextIndex + 1].nodes)
: parentAnchor
if (newIndexToOldIndexMap[i] === 0) {
// mount new
mount(source, nextIndex, anchor)
} else if (moved) {
// move if:
// There is no stable subsequence (e.g. a reverse)
// OR current node is not among the stable sequence
if (j < 0 || i !== increasingNewIndexSequence[j]) {
insert(newBlocks[nextIndex].nodes, parent!, anchor)
} else {
j--
}
}
}
}
}
}
frag.nodes = [(oldBlocks = newBlocks)]
2025-01-29 19:07:40 +08:00
if (parentAnchor) {
frag.nodes.push(parentAnchor)
2025-01-29 19:07:40 +08:00
}
}
const needKey = renderItem.length > 1
const needIndex = renderItem.length > 2
2025-01-29 19:07:40 +08:00
const mount = (
2025-01-30 22:36:41 +08:00
source: ResolvedSource,
2025-01-29 19:07:40 +08:00
idx: number,
anchor: Node | undefined = parentAnchor,
): ForBlock => {
const [item, key, index] = getItem(source, idx)
const itemRef = shallowRef(item)
// avoid creating refs if the render fn doesn't need it
const keyRef = needKey ? shallowRef(key) : undefined
const indexRef = needIndex ? shallowRef(index) : undefined
2025-01-29 19:07:40 +08:00
let nodes: Block
let scope: EffectScope | undefined
if (isComponent) {
// component already has its own scope so no outer scope needed
nodes = renderItem(itemRef, keyRef as any, indexRef as any)
2025-01-29 19:07:40 +08:00
} else {
scope = new EffectScope()
nodes = scope.run(() =>
renderItem(itemRef, keyRef as any, indexRef as any),
)!
2025-01-29 19:07:40 +08:00
}
const block = (newBlocks[idx] = new ForBlock(
nodes,
scope,
itemRef,
keyRef,
indexRef,
2025-01-29 19:07:40 +08:00
getKey && getKey(item, key, index),
))
if (parent) insert(block.nodes, parent, anchor)
return block
}
const tryPatchIndex = (source: any, 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)
return true
}
}
const update = (
{ itemRef, keyRef, indexRef }: ForBlock,
2025-01-29 19:07:40 +08:00
newItem: any,
newKey?: any,
newIndex?: any,
2025-01-29 19:07:40 +08:00
) => {
if (newIndex !== itemRef.value) {
itemRef.value = newItem
}
if (keyRef && newKey !== undefined && newKey !== keyRef.value) {
keyRef.value = newKey
}
if (indexRef && newIndex !== undefined && newIndex !== indexRef.value) {
indexRef.value = newIndex
2025-01-29 19:07:40 +08:00
}
}
const unmount = ({ nodes, scope }: ForBlock) => {
scope && scope.stop()
removeBlock(nodes, parent!)
2025-01-29 19:07:40 +08:00
}
once ? renderList() : renderEffect(renderList)
return frag
2025-01-29 19:07:40 +08:00
}
export function createForSlots(
2025-01-30 22:36:41 +08:00
rawSource: Source,
2025-01-29 19:07:40 +08:00
getSlot: (item: any, key: any, index?: number) => DynamicSlot,
): DynamicSlot[] {
2025-01-30 22:36:41 +08:00
const source = normalizeSource(rawSource)
const sourceLength = source.values.length
2025-01-29 19:07:40 +08:00
const slots = new Array<DynamicSlot>(sourceLength)
for (let i = 0; i < sourceLength; i++) {
2025-01-30 22:36:41 +08:00
slots[i] = getSlot(...getItem(source, i))
2025-01-29 19:07:40 +08:00
}
return slots
}
2025-01-30 22:36:41 +08:00
function normalizeSource(source: any): ResolvedSource {
let values = source
let needsWrap = false
let keys
if (isArray(source)) {
if (isReactive(source)) {
needsWrap = !isShallow(source)
values = shallowReadArray(source)
}
} else if (isString(source)) {
values = source.split('')
2025-01-29 19:07:40 +08:00
} else if (typeof source === 'number') {
if (__DEV__ && !Number.isInteger(source)) {
warn(`The v-for range expect an integer value but got ${source}.`)
}
2025-01-30 22:36:41 +08:00
values = new Array(source)
for (let i = 0; i < source; i++) values[i] = i + 1
2025-01-29 19:07:40 +08:00
} else if (isObject(source)) {
if (source[Symbol.iterator as any]) {
2025-01-30 22:36:41 +08:00
values = Array.from(source as Iterable<any>)
2025-01-29 19:07:40 +08:00
} else {
2025-01-30 22:36:41 +08:00
keys = Object.keys(source)
values = new Array(keys.length)
for (let i = 0, l = keys.length; i < l; i++) {
values[i] = source[keys[i]]
}
2025-01-29 19:07:40 +08:00
}
}
2025-01-30 22:36:41 +08:00
return { values, needsWrap, keys }
2025-01-29 19:07:40 +08:00
}
function getItem(
2025-01-30 22:36:41 +08:00
{ keys, values, needsWrap }: ResolvedSource,
2025-01-29 19:07:40 +08:00
idx: number,
): [item: any, key: any, index?: number] {
2025-01-30 22:36:41 +08:00
const value = needsWrap ? toReactive(values[idx]) : values[idx]
if (keys) {
return [value, keys[idx], idx]
} else {
return [value, idx, undefined]
2025-01-29 19:07:40 +08:00
}
}
function normalizeAnchor(node: Block): Node {
if (node instanceof Node) {
return node
} else if (isArray(node)) {
return normalizeAnchor(node[0])
} else if (isVaporComponent(node)) {
return normalizeAnchor(node.block!)
} else {
return normalizeAnchor(node.nodes!)
}
2024-12-11 11:50:17 +08:00
}
// runtime helper for rest element destructure
export function getRestElement(val: any, keys: string[]): any {
const res: any = {}
for (const key in val) {
if (!keys.includes(key)) res[key] = val[key]
}
return res
}
export function getDefaultValue(val: any, defaultVal: any): any {
return val === undefined ? defaultVal : val
}