Przeglądaj źródła

feat: v-once for component and v-for (#201)

* feat: v-once with v-for / v-once for component

* refactor

* refactor

---------

Co-authored-by: 三咲智子 Kevin Deng <sxzz@sxzz.moe>
Jevon 2 lat temu
rodzic
commit
b776f92596

+ 28 - 0
packages/compiler-vapor/__tests__/transforms/__snapshots__/vOnce.spec.ts.snap

@@ -35,6 +35,21 @@ export function render(_ctx) {
 }"
 `;
 
+exports[`compiler: v-once > on component 1`] = `
+"import { resolveComponent as _resolveComponent, createComponent as _createComponent, insert as _insert, template as _template } from 'vue/vapor';
+const t0 = _template("<div></div>")
+
+export function render(_ctx) {
+  const _component_Comp = _resolveComponent("Comp")
+  const n1 = t0()
+  const n0 = _createComponent(_component_Comp, [
+    { id: () => (_ctx.foo) }
+  ], null, null, null, true)
+  _insert(n0, n1)
+  return n1
+}"
+`;
+
 exports[`compiler: v-once > on nested plain element 1`] = `
 "import { setDynamicProp as _setDynamicProp, template as _template } from 'vue/vapor';
 const t0 = _template("<div><div></div></div>")
@@ -47,6 +62,19 @@ export function render(_ctx) {
 }"
 `;
 
+exports[`compiler: v-once > with v-for 1`] = `
+"import { createFor as _createFor, template as _template } from 'vue/vapor';
+const t0 = _template("<div></div>")
+
+export function render(_ctx) {
+  const n0 = _createFor(() => (_ctx.list), (_block) => {
+    const n2 = t0()
+    return [n2, () => {}]
+  }, null, null, null, true)
+  return n0
+}"
+`;
+
 exports[`compiler: v-once > with v-if 1`] = `
 "import { createIf as _createIf, template as _template } from 'vue/vapor';
 const t0 = _template("<div></div>")

+ 31 - 2
packages/compiler-vapor/__tests__/transforms/vOnce.spec.ts

@@ -131,7 +131,25 @@ describe('compiler: v-once', () => {
     ])
   })
 
-  test.todo('on component')
+  test('on component', () => {
+    const { ir, code } = compileWithOnce(`<div><Comp :id="foo" v-once /></div>`)
+    expect(code).toMatchSnapshot()
+    expect(ir.block.effect).lengthOf(0)
+    expect(ir.block.operation).toMatchObject([
+      {
+        type: IRNodeTypes.CREATE_COMPONENT_NODE,
+        id: 0,
+        tag: 'Comp',
+        once: true,
+      },
+      {
+        type: IRNodeTypes.INSERT_NODE,
+        elements: [0],
+        parent: 1,
+      },
+    ])
+  })
+
   test.todo('on slot outlet')
 
   test('inside v-once', () => {
@@ -205,5 +223,16 @@ describe('compiler: v-once', () => {
     ])
   })
 
-  test.todo('with v-for')
+  test('with v-for', () => {
+    const { ir, code } = compileWithOnce(`<div v-for="i in list" v-once />`)
+    expect(code).toMatchSnapshot()
+    expect(ir.block.effect).lengthOf(0)
+    expect(ir.block.operation).toMatchObject([
+      {
+        type: IRNodeTypes.FOR,
+        id: 0,
+        once: true,
+      },
+    ])
+  })
 })

+ 3 - 2
packages/compiler-vapor/src/generators/component.ts

@@ -34,7 +34,7 @@ export function genCreateComponent(
   const { vaporHelper } = context
 
   const tag = genTag()
-  const { root, slots, dynamicSlots } = oper
+  const { root, slots, dynamicSlots, once } = oper
   const rawProps = genRawProps(oper.props, context)
 
   return [
@@ -46,7 +46,8 @@ export function genCreateComponent(
       rawProps,
       slots && genSlots(slots, context),
       dynamicSlots && genDynamicSlots(dynamicSlots, context),
-      root && 'true',
+      root ? 'true' : false,
+      once && 'true',
     ),
     ...genDirectivesForElement(oper.id, context),
   ]

+ 10 - 3
packages/compiler-vapor/src/generators/for.ts

@@ -19,7 +19,7 @@ export function genFor(
   context: CodegenContext,
 ): CodeFragment[] {
   const { vaporHelper, genEffects } = context
-  const { source, value, key, index, render, keyProp } = oper
+  const { source, value, key, index, render, keyProp, once } = oper
 
   const rawValue = value && value.content
   const rawKey = key && key.content
@@ -67,11 +67,18 @@ export function genFor(
   }
 
   genEffects.pop()
-
   return [
     NEWLINE,
     `const n${oper.id} = `,
-    ...genCall(vaporHelper('createFor'), sourceExpr, blockFn, getKeyFn),
+    ...genCall(
+      vaporHelper('createFor'),
+      sourceExpr,
+      blockFn,
+      getKeyFn,
+      false, // todo: getMemo
+      false, // todo: hydrationNode
+      once && 'true',
+    ),
   ]
 
   function genEffectInFor(effects: IREffect[]): CodeFragment[] {

+ 2 - 0
packages/compiler-vapor/src/ir.ts

@@ -82,6 +82,7 @@ export interface ForIRNode extends BaseIRNode {
   index?: SimpleExpressionNode
   keyProp?: SimpleExpressionNode
   render: BlockIRNode
+  once: boolean
 }
 
 export interface IRProp extends Omit<DirectiveTransformResult, 'value'> {
@@ -224,6 +225,7 @@ export interface CreateComponentIRNode extends BaseIRNode {
 
   asset: boolean
   root: boolean
+  once: boolean
 }
 
 export interface DeclareOldRefIRNode extends BaseIRNode {

+ 1 - 0
packages/compiler-vapor/src/transforms/transformElement.ts

@@ -106,6 +106,7 @@ function transformComponentElement(
     root,
     slots: context.slots,
     dynamicSlots: context.dynamicSlots,
+    once: context.inVOnce,
   })
   context.slots = undefined
   context.dynamicSlots = undefined

+ 1 - 0
packages/compiler-vapor/src/transforms/vFor.ts

@@ -63,6 +63,7 @@ export function processFor(
       index: index as SimpleExpressionNode | undefined,
       keyProp: keyProperty,
       render,
+      once: context.inVOnce,
     })
   }
 }

+ 2 - 0
packages/runtime-vapor/src/apiCreateComponent.ts

@@ -14,6 +14,7 @@ export function createComponent(
   slots: Slots | null = null,
   dynamicSlots: DynamicSlots | null = null,
   singleRoot: boolean = false,
+  once: boolean = false,
 ) {
   const current = currentInstance!
   const instance = createComponentInstance(
@@ -21,6 +22,7 @@ export function createComponent(
     singleRoot ? withAttrs(rawProps) : rawProps,
     slots,
     dynamicSlots,
+    once,
   )
   setupComponent(instance, singleRoot)
 

+ 7 - 5
packages/runtime-vapor/src/apiCreateFor.ts

@@ -22,6 +22,7 @@ export const createFor = (
   getKey?: (item: any, key: any, index?: number) => any,
   getMemo?: (item: any, key: any, index?: number) => any[],
   hydrationNode?: Node,
+  once?: boolean,
 ): Fragment => {
   let isMounted = false
   let oldBlocks: ForBlock[] = []
@@ -32,9 +33,12 @@ export const createFor = (
     nodes: oldBlocks,
     [fragmentKey]: true,
   }
-
   const update = getMemo ? updateWithMemo : updateWithoutMemo
-  renderEffect(() => {
+  once ? renderList() : renderEffect(renderList)
+
+  return ref
+
+  function renderList() {
     const source = src()
     const newLength = getLength(source)
     const oldLength = oldBlocks.length
@@ -213,9 +217,7 @@ export const createFor = (
     }
 
     ref.nodes = [(oldBlocks = newBlocks), parentAnchor]
-  })
-
-  return ref
+  }
 
   function mount(
     source: any,

+ 1 - 0
packages/runtime-vapor/src/apiCreateVaporApp.ts

@@ -47,6 +47,7 @@ export function createVaporApp(
           rootProps,
           null,
           null,
+          false,
           context,
         )
         setupComponent(instance)

+ 2 - 1
packages/runtime-vapor/src/component.ts

@@ -261,6 +261,7 @@ export function createComponentInstance(
   rawProps: RawProps | null,
   slots: Slots | null,
   dynamicSlots: DynamicSlots | null,
+  once: boolean = false,
   // application root node only
   appContext?: AppContext,
 ): ComponentInternalInstance {
@@ -354,7 +355,7 @@ export function createComponentInstance(
      */
     // [VaporLifecycleHooks.SERVER_PREFETCH]: null,
   }
-  initProps(instance, rawProps, !isFunction(component))
+  initProps(instance, rawProps, !isFunction(component), once)
   initSlots(instance, slots, dynamicSlots)
   instance.emit = emit.bind(null, instance)
 

+ 8 - 7
packages/runtime-vapor/src/componentProps.ts

@@ -81,6 +81,7 @@ export function initProps(
   instance: ComponentInternalInstance,
   rawProps: RawProps,
   isStateful: boolean,
+  once: boolean,
 ) {
   if (!rawProps) rawProps = []
   else if (!isArray(rawProps)) rawProps = [rawProps]
@@ -97,16 +98,16 @@ export function initProps(
       for (const key in options) {
         const getter = () =>
           getDynamicPropValue(rawProps as NormalizedRawProps, key)
-        registerProp(instance, props, key, getter, true)
+        registerProp(instance, once, props, key, getter, true)
       }
     } else {
       const staticProps = rawProps[0] as StaticProps
       for (const key in options) {
         const rawKey = staticProps && getRawKey(staticProps, key)
         if (rawKey) {
-          registerProp(instance, props, key, staticProps[rawKey])
+          registerProp(instance, once, props, key, staticProps[rawKey])
         } else {
-          registerProp(instance, props, key, undefined, false, true)
+          registerProp(instance, once, props, key, undefined, false, true)
         }
       }
     }
@@ -133,6 +134,7 @@ export function initProps(
 
 function registerProp(
   instance: ComponentInternalInstance,
+  once: boolean,
   props: Data,
   rawKey: string,
   getter?: (() => unknown) | (() => DynamicPropResult),
@@ -158,10 +160,9 @@ function registerProp(
         ? () => withCast(getter!())
         : getter!
 
-    Object.defineProperty(props, key, {
-      get,
-      enumerable: true,
-    })
+    const descriptor: PropertyDescriptor = once ? { value: get() } : { get }
+    descriptor.enumerable = true
+    Object.defineProperty(props, key, descriptor)
   }
 }