feat: v-memo

This commit is contained in:
Evan You
2021-07-09 21:41:44 -04:00
parent 5cea9a1d4e
commit 3b64508e3b
23 changed files with 563 additions and 131 deletions

View File

@@ -6,7 +6,8 @@ import {
RENDER_LIST,
OPEN_BLOCK,
FRAGMENT,
WITH_DIRECTIVES
WITH_DIRECTIVES,
WITH_MEMO
} from './runtimeHelpers'
import { PropsExpression } from './transforms/transformElement'
import { ImportItem, TransformContext } from './transform'
@@ -135,6 +136,7 @@ export interface PlainElementNode extends BaseElementNode {
| VNodeCall
| SimpleExpressionNode // when hoisted
| CacheExpression // when cached by v-once
| MemoExpression // when cached by v-memo
| undefined
ssrCodegenNode?: TemplateLiteral
}
@@ -144,6 +146,7 @@ export interface ComponentNode extends BaseElementNode {
codegenNode:
| VNodeCall
| CacheExpression // when cached by v-once
| MemoExpression // when cached by v-memo
| undefined
ssrCodegenNode?: CallExpression
}
@@ -375,6 +378,15 @@ export interface CacheExpression extends Node {
isVNode: boolean
}
export interface MemoExpression extends CallExpression {
callee: typeof WITH_MEMO
arguments: [ExpressionNode, MemoFactory, string, string]
}
interface MemoFactory extends FunctionExpression {
returns: BlockCodegenNode
}
// SSR-specific Node Types -----------------------------------------------------
export type SSRCodegenNode =
@@ -499,8 +511,8 @@ export interface DynamicSlotFnProperty extends Property {
export type BlockCodegenNode = VNodeCall | RenderSlotCall
export interface IfConditionalExpression extends ConditionalExpression {
consequent: BlockCodegenNode
alternate: BlockCodegenNode | IfConditionalExpression
consequent: BlockCodegenNode | MemoExpression
alternate: BlockCodegenNode | IfConditionalExpression | MemoExpression
}
export interface ForCodegenNode extends VNodeCall {
@@ -627,7 +639,7 @@ export function createObjectProperty(
export function createSimpleExpression(
content: SimpleExpressionNode['content'],
isStatic: SimpleExpressionNode['isStatic'],
isStatic: SimpleExpressionNode['isStatic'] = false,
loc: SourceLocation = locStub,
constType: ConstantTypes = ConstantTypes.NOT_CONSTANT
): SimpleExpressionNode {

View File

@@ -651,11 +651,11 @@ function genNode(node: CodegenNode | symbol | string, context: CodegenContext) {
case NodeTypes.JS_CACHE_EXPRESSION:
genCacheExpression(node, context)
break
case NodeTypes.JS_BLOCK_STATEMENT:
genNodeList(node.body, context, true, false)
break
// SSR only types
case NodeTypes.JS_BLOCK_STATEMENT:
!__BROWSER__ && genNodeList(node.body, context, true, false)
break
case NodeTypes.JS_TEMPLATE_LITERAL:
!__BROWSER__ && genTemplateLiteral(node, context)
break

View File

@@ -17,6 +17,7 @@ import { transformOnce } from './transforms/vOnce'
import { transformModel } from './transforms/vModel'
import { transformFilter } from './compat/transformFilter'
import { defaultOnError, createCompilerError, ErrorCodes } from './errors'
import { transformMemo } from './transforms/vMemo'
export type TransformPreset = [
NodeTransform[],
@@ -30,6 +31,7 @@ export function getBaseTransformPreset(
[
transformOnce,
transformIf,
transformMemo,
transformFor,
...(__COMPAT__ ? [transformFilter] : []),
...(!__BROWSER__ && prefixIdentifiers

View File

@@ -38,6 +38,8 @@ export const WITH_SCOPE_ID = Symbol(__DEV__ ? `withScopeId` : ``)
export const WITH_CTX = Symbol(__DEV__ ? `withCtx` : ``)
export const UNREF = Symbol(__DEV__ ? `unref` : ``)
export const IS_REF = Symbol(__DEV__ ? `isRef` : ``)
export const WITH_MEMO = Symbol(__DEV__ ? `withMemo` : ``)
export const IS_MEMO_SAME = Symbol(__DEV__ ? `isMemoSame` : ``)
// Name mapping for runtime helpers that need to be imported from 'vue' in
// generated code. Make sure these are correctly exported in the runtime!
@@ -80,7 +82,9 @@ export const helperNameMap: any = {
[WITH_SCOPE_ID]: `withScopeId`,
[WITH_CTX]: `withCtx`,
[UNREF]: `unref`,
[IS_REF]: `isRef`
[IS_REF]: `isRef`,
[WITH_MEMO]: `withMemo`,
[IS_MEMO_SAME]: `isMemoSame`
}
export function registerRuntimeHelpers(helpers: any) {

View File

@@ -34,10 +34,9 @@ import {
TO_DISPLAY_STRING,
FRAGMENT,
helperNameMap,
CREATE_COMMENT,
OPEN_BLOCK
CREATE_COMMENT
} from './runtimeHelpers'
import { getVNodeBlockHelper, getVNodeHelper, isVSlot } from './utils'
import { isVSlot, makeBlock } from './utils'
import { hoistStatic, isSingleElementRoot } from './transforms/hoistStatic'
import { CompilerCompatOptions } from './compat/compatConfig'
@@ -278,7 +277,7 @@ export function createTransformContext(
}
},
hoist(exp) {
if (isString(exp)) exp = createSimpleExpression(exp, false)
if (isString(exp)) exp = createSimpleExpression(exp)
context.hoists.push(exp)
const identifier = createSimpleExpression(
`_hoisted_${context.hoists.length}`,
@@ -290,7 +289,7 @@ export function createTransformContext(
return identifier
},
cache(exp, isVNode = false) {
return createCacheExpression(++context.cached, exp, isVNode)
return createCacheExpression(context.cached++, exp, isVNode)
}
}
@@ -337,7 +336,7 @@ export function transform(root: RootNode, options: TransformOptions) {
}
function createRootCodegen(root: RootNode, context: TransformContext) {
const { helper, removeHelper } = context
const { helper } = context
const { children } = root
if (children.length === 1) {
const child = children[0]
@@ -347,12 +346,7 @@ function createRootCodegen(root: RootNode, context: TransformContext) {
// SimpleExpressionNode
const codegenNode = child.codegenNode
if (codegenNode.type === NodeTypes.VNODE_CALL) {
if (!codegenNode.isBlock) {
codegenNode.isBlock = true
removeHelper(getVNodeHelper(context.inSSR, codegenNode.isComponent))
helper(OPEN_BLOCK)
helper(getVNodeBlockHelper(context.inSSR, codegenNode.isComponent))
}
makeBlock(codegenNode, context)
}
root.codegenNode = codegenNode
} else {

View File

@@ -504,8 +504,8 @@ export function buildProps(
}
continue
}
// skip v-once - it is handled by its dedicated transform.
if (name === 'once') {
// skip v-once/v-memo - they are handled by dedicated transforms.
if (name === 'once' || name === 'memo') {
continue
}
// skip v-is and :is on <component>

View File

@@ -24,7 +24,9 @@ import {
ForRenderListExpression,
BlockCodegenNode,
ForIteratorExpression,
ConstantTypes
ConstantTypes,
createBlockStatement,
createCompoundExpression
} from '../ast'
import { createCompilerError, ErrorCodes } from '../errors'
import {
@@ -34,9 +36,15 @@ import {
isSlotOutlet,
injectProp,
getVNodeBlockHelper,
getVNodeHelper
getVNodeHelper,
findDir
} from '../utils'
import { RENDER_LIST, OPEN_BLOCK, FRAGMENT } from '../runtimeHelpers'
import {
RENDER_LIST,
OPEN_BLOCK,
FRAGMENT,
IS_MEMO_SAME
} from '../runtimeHelpers'
import { processExpression } from './transformExpression'
import { validateBrowserExpression } from '../validateExpression'
import { PatchFlags, PatchFlagNames } from '@vue/shared'
@@ -51,15 +59,14 @@ export const transformFor = createStructuralDirectiveTransform(
const renderExp = createCallExpression(helper(RENDER_LIST), [
forNode.source
]) as ForRenderListExpression
const memo = findDir(node, 'memo')
const keyProp = findProp(node, `key`)
const keyProperty = keyProp
? createObjectProperty(
`key`,
keyProp.type === NodeTypes.ATTRIBUTE
? createSimpleExpression(keyProp.value!.content, true)
: keyProp.exp!
)
: null
const keyExp =
keyProp &&
(keyProp.type === NodeTypes.ATTRIBUTE
? createSimpleExpression(keyProp.value!.content, true)
: keyProp.exp!)
const keyProperty = keyProp ? createObjectProperty(`key`, keyExp!) : null
if (!__BROWSER__ && context.prefixIdentifiers && keyProperty) {
// #2085 process :key expression needs to be processed in order for it
@@ -189,11 +196,37 @@ export const transformFor = createStructuralDirectiveTransform(
}
}
renderExp.arguments.push(createFunctionExpression(
createForLoopParams(forNode.parseResult),
childBlock,
true /* force newline */
) as ForIteratorExpression)
if (memo) {
const loop = createFunctionExpression(
createForLoopParams(forNode.parseResult, [
createSimpleExpression(`_cached`)
])
)
loop.body = createBlockStatement([
createCompoundExpression([`const _memo = (`, memo.exp!, `)`]),
createCompoundExpression([
`if (_cached`,
...(keyExp ? [` && _cached.key === `, keyExp] : []),
` && ${context.helperString(
IS_MEMO_SAME
)}(_cached.memo, _memo)) return _cached`
]),
createCompoundExpression([`const _item = `, childBlock as any]),
createSimpleExpression(`_item.memo = _memo`),
createSimpleExpression(`return _item`)
])
renderExp.arguments.push(
loop as ForIteratorExpression,
createSimpleExpression(`_cache`),
createSimpleExpression(String(context.cached++))
)
} else {
renderExp.arguments.push(createFunctionExpression(
createForLoopParams(forNode.parseResult),
childBlock,
true /* force newline */
) as ForIteratorExpression)
}
}
})
}
@@ -393,29 +426,21 @@ function createAliasExpression(
)
}
export function createForLoopParams({
value,
key,
index
}: ForParseResult): ExpressionNode[] {
const params: ExpressionNode[] = []
if (value) {
params.push(value)
}
if (key) {
if (!value) {
params.push(createSimpleExpression(`_`, false))
}
params.push(key)
}
if (index) {
if (!key) {
if (!value) {
params.push(createSimpleExpression(`_`, false))
}
params.push(createSimpleExpression(`__`, false))
}
params.push(index)
}
return params
export function createForLoopParams(
{ value, key, index }: ForParseResult,
memoArgs: ExpressionNode[] = []
): ExpressionNode[] {
return createParamsList([value, key, index, ...memoArgs])
}
function createParamsList(
args: (ExpressionNode | undefined)[]
): ExpressionNode[] {
let i = args.length
while (i--) {
if (args[i]) break
}
return args
.slice(0, i + 1)
.map((arg, i) => arg || createSimpleExpression(`_`.repeat(i + 1), false))
}

View File

@@ -22,21 +22,22 @@ import {
AttributeNode,
locStub,
CacheExpression,
ConstantTypes
ConstantTypes,
MemoExpression
} from '../ast'
import { createCompilerError, ErrorCodes } from '../errors'
import { processExpression } from './transformExpression'
import { validateBrowserExpression } from '../validateExpression'
import { FRAGMENT, CREATE_COMMENT, OPEN_BLOCK } from '../runtimeHelpers'
import { FRAGMENT, CREATE_COMMENT } from '../runtimeHelpers'
import {
injectProp,
findDir,
findProp,
isBuiltInType,
getVNodeHelper,
getVNodeBlockHelper
makeBlock
} from '../utils'
import { PatchFlags, PatchFlagNames } from '@vue/shared'
import { getMemoedVNodeCall } from '..'
export const transformIf = createStructuralDirectiveTransform(
/^(if|else|else-if)$/,
@@ -214,7 +215,7 @@ function createCodegenNodeForBranch(
branch: IfBranchNode,
keyIndex: number,
context: TransformContext
): IfConditionalExpression | BlockCodegenNode {
): IfConditionalExpression | BlockCodegenNode | MemoExpression {
if (branch.condition) {
return createConditionalExpression(
branch.condition,
@@ -235,8 +236,8 @@ function createChildrenCodegenNode(
branch: IfBranchNode,
keyIndex: number,
context: TransformContext
): BlockCodegenNode {
const { helper, removeHelper } = context
): BlockCodegenNode | MemoExpression {
const { helper } = context
const keyProperty = createObjectProperty(
`key`,
createSimpleExpression(
@@ -284,18 +285,17 @@ function createChildrenCodegenNode(
)
}
} else {
const vnodeCall = (firstChild as ElementNode)
.codegenNode as BlockCodegenNode
const ret = (firstChild as ElementNode).codegenNode as
| BlockCodegenNode
| MemoExpression
const vnodeCall = getMemoedVNodeCall(ret)
// Change createVNode to createBlock.
if (vnodeCall.type === NodeTypes.VNODE_CALL && !vnodeCall.isBlock) {
removeHelper(getVNodeHelper(context.inSSR, vnodeCall.isComponent))
vnodeCall.isBlock = true
helper(OPEN_BLOCK)
helper(getVNodeBlockHelper(context.inSSR, vnodeCall.isComponent))
if (vnodeCall.type === NodeTypes.VNODE_CALL) {
makeBlock(vnodeCall, context)
}
// inject branch key
injectProp(vnodeCall, keyProperty, context)
return vnodeCall
return ret
}
}

View File

@@ -0,0 +1,40 @@
import { NodeTransform } from '../transform'
import { findDir, makeBlock } from '../utils'
import {
createCallExpression,
createFunctionExpression,
ElementTypes,
MemoExpression,
NodeTypes,
PlainElementNode
} from '../ast'
import { WITH_MEMO } from '../runtimeHelpers'
const seen = new WeakSet()
export const transformMemo: NodeTransform = (node, context) => {
if (node.type === NodeTypes.ELEMENT) {
const dir = findDir(node, 'memo')
if (!dir || seen.has(node)) {
return
}
seen.add(node)
return () => {
const codegenNode =
node.codegenNode ||
(context.currentNode as PlainElementNode).codegenNode
if (codegenNode && codegenNode.type === NodeTypes.VNODE_CALL) {
// non-component sub tree should be turned into a block
if (node.tagType !== ElementTypes.COMPONENT) {
makeBlock(codegenNode, context)
}
node.codegenNode = createCallExpression(context.helper(WITH_MEMO), [
dir.exp!,
createFunctionExpression(undefined, codegenNode),
`_cache`,
String(context.cached++)
]) as MemoExpression
}
}
}
}

View File

@@ -21,7 +21,9 @@ import {
TextNode,
InterpolationNode,
VNodeCall,
SimpleExpressionNode
SimpleExpressionNode,
BlockCodegenNode,
MemoExpression
} from './ast'
import { TransformContext } from './transform'
import {
@@ -36,7 +38,9 @@ import {
CREATE_BLOCK,
CREATE_ELEMENT_BLOCK,
CREATE_VNODE,
CREATE_ELEMENT_VNODE
CREATE_ELEMENT_VNODE,
WITH_MEMO,
OPEN_BLOCK
} from './runtimeHelpers'
import { isString, isObject, hyphenate, extend } from '@vue/shared'
import { PropsExpression } from './transforms/transformElement'
@@ -483,3 +487,23 @@ export function hasScopeRef(
return false
}
}
export function getMemoedVNodeCall(node: BlockCodegenNode | MemoExpression) {
if (node.type === NodeTypes.JS_CALL_EXPRESSION && node.callee === WITH_MEMO) {
return node.arguments[1].returns as VNodeCall
} else {
return node
}
}
export function makeBlock(
node: VNodeCall,
{ helper, removeHelper, inSSR }: TransformContext
) {
if (!node.isBlock) {
node.isBlock = true
removeHelper(getVNodeHelper(inSSR, node.isComponent))
helper(OPEN_BLOCK)
helper(getVNodeBlockHelper(inSSR, node.isComponent))
}
}