fix(compiler): avoid hoisting components and directive calls

This commit is contained in:
Evan You 2019-10-04 14:34:26 -04:00
parent 5047bc8dbe
commit 277651ce89
5 changed files with 66 additions and 38 deletions

View File

@ -89,7 +89,8 @@ return function render() {
`; `;
exports[`compiler: codegen hoists 1`] = ` exports[`compiler: codegen hoists 1`] = `
"const _hoisted_1 = hello "
const _hoisted_1 = hello
const _hoisted_2 = { id: \\"foo\\" } const _hoisted_2 = { id: \\"foo\\" }
return function render() { return function render() {

View File

@ -35,7 +35,7 @@ return function render() {
class: _ctx.bar.baz class: _ctx.bar.baz
}, [ }, [
toString(_ctx.world.burn()), toString(_ctx.world.burn()),
(openBlock(), (_ctx.ok) (openBlock(), _ctx.ok)
? createBlock(\\"div\\", { key: 0 }, \\"yes\\") ? createBlock(\\"div\\", { key: 0 }, \\"yes\\")
: createBlock(Fragment, { key: 1 }, [\\"no\\"])), : createBlock(Fragment, { key: 1 }, [\\"no\\"])),
(openBlock(), createBlock(Fragment, null, renderList(_ctx.list, (value, index) => { (openBlock(), createBlock(Fragment, null, renderList(_ctx.list, (value, index) => {
@ -57,7 +57,7 @@ export default function render() {
class: _ctx.bar.baz class: _ctx.bar.baz
}, [ }, [
_toString(_ctx.world.burn()), _toString(_ctx.world.burn()),
(openBlock(), (_ctx.ok) (openBlock(), _ctx.ok)
? createBlock(\\"div\\", { key: 0 }, \\"yes\\") ? createBlock(\\"div\\", { key: 0 }, \\"yes\\")
: createBlock(Fragment, { key: 1 }, [\\"no\\"])), : createBlock(Fragment, { key: 1 }, [\\"no\\"])),
(openBlock(), createBlock(Fragment, null, renderList(_ctx.list, (value, index) => { (openBlock(), createBlock(Fragment, null, renderList(_ctx.list, (value, index) => {

View File

@ -71,7 +71,7 @@ return function render() {
const _component_Comp = resolveComponent(\\"Comp\\") const _component_Comp = resolveComponent(\\"Comp\\")
return (openBlock(), createBlock(_component_Comp, null, createSlots({ _compiled: true }, [ return (openBlock(), createBlock(_component_Comp, null, createSlots({ _compiled: true }, [
(_ctx.ok) _ctx.ok)
? { ? {
name: \\"one\\", name: \\"one\\",
fn: (props) => [toString(props)] fn: (props) => [toString(props)]

View File

@ -197,6 +197,7 @@ export function generate(
} }
} }
genHoists(ast.hoists, context) genHoists(ast.hoists, context)
context.newline()
push(`return `) push(`return `)
} else { } else {
// generate import statements for helpers // generate import statements for helpers
@ -204,6 +205,7 @@ export function generate(
push(`import { ${ast.imports.join(', ')} } from "vue"\n`) push(`import { ${ast.imports.join(', ')} } from "vue"\n`)
} }
genHoists(ast.hoists, context) genHoists(ast.hoists, context)
context.newline()
push(`export default `) push(`export default `)
} }
@ -258,12 +260,15 @@ export function generate(
} }
function genHoists(hoists: JSChildNode[], context: CodegenContext) { function genHoists(hoists: JSChildNode[], context: CodegenContext) {
if (!hoists.length) {
return
}
context.newline()
hoists.forEach((exp, i) => { hoists.forEach((exp, i) => {
context.push(`const _hoisted_${i + 1} = `) context.push(`const _hoisted_${i + 1} = `)
genNode(exp, context) genNode(exp, context)
context.newline() context.newline()
}) })
context.newline()
} }
function isText(n: string | CodegenNode) { function isText(n: string | CodegenNode) {
@ -316,7 +321,11 @@ function genNodeList(
} }
} }
function genNode(node: CodegenNode, context: CodegenContext) { function genNode(node: CodegenNode | string, context: CodegenContext) {
if (isString(node)) {
context.push(node)
return
}
switch (node.type) { switch (node.type) {
case NodeTypes.ELEMENT: case NodeTypes.ELEMENT:
case NodeTypes.IF: case NodeTypes.IF:
@ -517,12 +526,13 @@ function genConditionalExpression(
const { test, consequent, alternate } = node const { test, consequent, alternate } = node
const { push, indent, deindent, newline } = context const { push, indent, deindent, newline } = context
if (test.type === NodeTypes.SIMPLE_EXPRESSION) { if (test.type === NodeTypes.SIMPLE_EXPRESSION) {
const needsQuote = !isSimpleIdentifier(test.content) const needsParens = !isSimpleIdentifier(test.content)
needsQuote && push(`(`)
genExpression(test, context) genExpression(test, context)
needsQuote && push(`)`) needsParens && push(`)`)
} else { } else {
push(`(`)
genCompoundExpression(test, context) genCompoundExpression(test, context)
push(`)`)
} }
indent() indent()
context.indentLevel++ context.indentLevel++

View File

@ -3,79 +3,96 @@ import {
NodeTypes, NodeTypes,
TemplateChildNode, TemplateChildNode,
CallExpression, CallExpression,
ElementNode ElementNode,
ElementTypes
} from '../ast' } from '../ast'
import { TransformContext } from '../transform' import { TransformContext } from '../transform'
import { CREATE_VNODE } from '../runtimeConstants' import { APPLY_DIRECTIVES } from '../runtimeConstants'
import { PropsExpression } from './transformElement' import { PropsExpression } from './transformElement'
import { PatchFlags } from '@vue/runtime-dom'
export function hoistStatic(root: RootNode, context: TransformContext) { export function hoistStatic(root: RootNode, context: TransformContext) {
walk(root.children, context, new Set<TemplateChildNode>()) walk(root.children, context, new Map<TemplateChildNode, boolean>())
} }
function walk( function walk(
children: TemplateChildNode[], children: TemplateChildNode[],
context: TransformContext, context: TransformContext,
knownStaticNodes: Set<TemplateChildNode> resultCache: Map<TemplateChildNode, boolean>
) { ) {
for (let i = 0; i < children.length; i++) { for (let i = 0; i < children.length; i++) {
const child = children[i] const child = children[i]
if (child.type === NodeTypes.ELEMENT) { // only plain elements are eligible for hoisting.
if (isStaticNode(child, knownStaticNodes)) { if (
child.type === NodeTypes.ELEMENT &&
child.tagType === ElementTypes.ELEMENT
) {
if (isStaticNode(child, resultCache)) {
// whole tree is static // whole tree is static
child.codegenNode = context.hoist(child.codegenNode!) child.codegenNode = context.hoist(child.codegenNode!)
continue continue
} else if (!getPatchFlag(child)) { } else {
// has dynamic children, but self props are static, hoist props instead // node may contain dynamic children, but its props may be eligible for
const props = (child.codegenNode as CallExpression).arguments[1] as // hoisting.
const flag = getPatchFlag(child)
if (!flag || flag === PatchFlags.NEED_PATCH) {
let codegenNode = child.codegenNode as CallExpression
if (codegenNode.callee.includes(APPLY_DIRECTIVES)) {
codegenNode = codegenNode.arguments[0] as CallExpression
}
const props = codegenNode.arguments[1] as
| PropsExpression | PropsExpression
| `null` | `null`
if (props !== `null`) { | undefined
if (props && props !== `null`) {
;(child.codegenNode as CallExpression).arguments[1] = context.hoist( ;(child.codegenNode as CallExpression).arguments[1] = context.hoist(
props props
) )
} }
} }
} }
}
if (child.type === NodeTypes.ELEMENT || child.type === NodeTypes.FOR) { if (child.type === NodeTypes.ELEMENT || child.type === NodeTypes.FOR) {
walk(child.children, context, knownStaticNodes) walk(child.children, context, resultCache)
} else if (child.type === NodeTypes.IF) { } else if (child.type === NodeTypes.IF) {
for (let i = 0; i < child.branches.length; i++) { for (let i = 0; i < child.branches.length; i++) {
walk(child.branches[i].children, context, knownStaticNodes) walk(child.branches[i].children, context, resultCache)
} }
} }
} }
} }
function getPatchFlag(node: ElementNode): number | undefined { function getPatchFlag(node: ElementNode): number | undefined {
const codegenNode = node.codegenNode as CallExpression let codegenNode = node.codegenNode as CallExpression
if ( if (codegenNode.callee.includes(APPLY_DIRECTIVES)) {
// callee is createVNode (i.e. no runtime directives) codegenNode = codegenNode.arguments[0] as CallExpression
codegenNode.callee.includes(CREATE_VNODE) }
) {
const flag = codegenNode.arguments[3] const flag = codegenNode.arguments[3]
return flag ? parseInt(flag as string, 10) : undefined return flag ? parseInt(flag as string, 10) : undefined
}
} }
function isStaticNode( function isStaticNode(
node: TemplateChildNode, node: TemplateChildNode,
knownStaticNodes: Set<TemplateChildNode> resultCache: Map<TemplateChildNode, boolean>
): boolean { ): boolean {
switch (node.type) { switch (node.type) {
case NodeTypes.ELEMENT: case NodeTypes.ELEMENT:
if (knownStaticNodes.has(node)) { if (node.tagType !== ElementTypes.ELEMENT) {
return true return false
}
if (resultCache.has(node)) {
return resultCache.get(node) as boolean
} }
const flag = getPatchFlag(node) const flag = getPatchFlag(node)
if (!flag) { if (!flag) {
// element self is static. check its children. // element self is static. check its children.
for (let i = 0; i < node.children.length; i++) { for (let i = 0; i < node.children.length; i++) {
if (!isStaticNode(node.children[i], knownStaticNodes)) { if (!isStaticNode(node.children[i], resultCache)) {
resultCache.set(node, false)
return false return false
} }
} }
knownStaticNodes.add(node) resultCache.set(node, true)
return true return true
} else { } else {
return false return false