fix(compiler-core): change node hoisting to caching per instance (#11067)

close #5256
close #9219
close #10959
This commit is contained in:
Evan You 2024-06-04 20:09:54 +08:00 committed by GitHub
parent f8eba75d0a
commit cd0ea0d479
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
35 changed files with 1195 additions and 1093 deletions

View File

@ -2,7 +2,7 @@
exports[`compiler: parse > Edge Cases > invalid html 1`] = ` exports[`compiler: parse > Edge Cases > invalid html 1`] = `
{ {
"cached": 0, "cached": [],
"children": [ "children": [
{ {
"children": [ "children": [
@ -86,7 +86,7 @@ exports[`compiler: parse > Edge Cases > invalid html 1`] = `
exports[`compiler: parse > Edge Cases > self closing multiple tag 1`] = ` exports[`compiler: parse > Edge Cases > self closing multiple tag 1`] = `
{ {
"cached": 0, "cached": [],
"children": [ "children": [
{ {
"children": [], "children": [],
@ -280,7 +280,7 @@ exports[`compiler: parse > Edge Cases > self closing multiple tag 1`] = `
exports[`compiler: parse > Edge Cases > valid html 1`] = ` exports[`compiler: parse > Edge Cases > valid html 1`] = `
{ {
"cached": 0, "cached": [],
"children": [ "children": [
{ {
"children": [ "children": [
@ -498,7 +498,7 @@ exports[`compiler: parse > Edge Cases > valid html 1`] = `
exports[`compiler: parse > Errors > CDATA_IN_HTML_CONTENT > <template><![CDATA[cdata]]></template> 1`] = ` exports[`compiler: parse > Errors > CDATA_IN_HTML_CONTENT > <template><![CDATA[cdata]]></template> 1`] = `
{ {
"cached": 0, "cached": [],
"children": [ "children": [
{ {
"children": [], "children": [],
@ -550,7 +550,7 @@ exports[`compiler: parse > Errors > CDATA_IN_HTML_CONTENT > <template><![CDATA[c
exports[`compiler: parse > Errors > CDATA_IN_HTML_CONTENT > <template><svg><![CDATA[cdata]]></svg></template> 1`] = ` exports[`compiler: parse > Errors > CDATA_IN_HTML_CONTENT > <template><svg><![CDATA[cdata]]></svg></template> 1`] = `
{ {
"cached": 0, "cached": [],
"children": [ "children": [
{ {
"children": [ "children": [
@ -643,7 +643,7 @@ exports[`compiler: parse > Errors > CDATA_IN_HTML_CONTENT > <template><svg><![CD
exports[`compiler: parse > Errors > DUPLICATE_ATTRIBUTE > <template><div id="" id=""></div></template> 1`] = ` exports[`compiler: parse > Errors > DUPLICATE_ATTRIBUTE > <template><div id="" id=""></div></template> 1`] = `
{ {
"cached": 0, "cached": [],
"children": [ "children": [
{ {
"children": [ "children": [
@ -813,7 +813,7 @@ exports[`compiler: parse > Errors > DUPLICATE_ATTRIBUTE > <template><div id="" i
exports[`compiler: parse > Errors > EOF_BEFORE_TAG_NAME > <template>< 1`] = ` exports[`compiler: parse > Errors > EOF_BEFORE_TAG_NAME > <template>< 1`] = `
{ {
"cached": 0, "cached": [],
"children": [ "children": [
{ {
"children": [ "children": [
@ -883,7 +883,7 @@ exports[`compiler: parse > Errors > EOF_BEFORE_TAG_NAME > <template>< 1`] = `
exports[`compiler: parse > Errors > EOF_BEFORE_TAG_NAME > <template></ 1`] = ` exports[`compiler: parse > Errors > EOF_BEFORE_TAG_NAME > <template></ 1`] = `
{ {
"cached": 0, "cached": [],
"children": [ "children": [
{ {
"children": [ "children": [
@ -953,7 +953,7 @@ exports[`compiler: parse > Errors > EOF_BEFORE_TAG_NAME > <template></ 1`] = `
exports[`compiler: parse > Errors > EOF_IN_CDATA > <template><svg><![CDATA[ 1`] = ` exports[`compiler: parse > Errors > EOF_IN_CDATA > <template><svg><![CDATA[ 1`] = `
{ {
"cached": 0, "cached": [],
"children": [ "children": [
{ {
"children": [ "children": [
@ -1028,7 +1028,7 @@ exports[`compiler: parse > Errors > EOF_IN_CDATA > <template><svg><![CDATA[ 1`]
exports[`compiler: parse > Errors > EOF_IN_CDATA > <template><svg><![CDATA[cdata 1`] = ` exports[`compiler: parse > Errors > EOF_IN_CDATA > <template><svg><![CDATA[cdata 1`] = `
{ {
"cached": 0, "cached": [],
"children": [ "children": [
{ {
"children": [ "children": [
@ -1121,7 +1121,7 @@ exports[`compiler: parse > Errors > EOF_IN_CDATA > <template><svg><![CDATA[cdata
exports[`compiler: parse > Errors > EOF_IN_COMMENT > <template><!-- 1`] = ` exports[`compiler: parse > Errors > EOF_IN_COMMENT > <template><!-- 1`] = `
{ {
"cached": 0, "cached": [],
"children": [ "children": [
{ {
"children": [], "children": [],
@ -1173,7 +1173,7 @@ exports[`compiler: parse > Errors > EOF_IN_COMMENT > <template><!-- 1`] = `
exports[`compiler: parse > Errors > EOF_IN_COMMENT > <template><!--comment 1`] = ` exports[`compiler: parse > Errors > EOF_IN_COMMENT > <template><!--comment 1`] = `
{ {
"cached": 0, "cached": [],
"children": [ "children": [
{ {
"children": [ "children": [
@ -1243,7 +1243,7 @@ exports[`compiler: parse > Errors > EOF_IN_COMMENT > <template><!--comment 1`] =
exports[`compiler: parse > Errors > EOF_IN_TAG > <div></div 1`] = ` exports[`compiler: parse > Errors > EOF_IN_TAG > <div></div 1`] = `
{ {
"cached": 0, "cached": [],
"children": [ "children": [
{ {
"children": [], "children": [],
@ -1295,7 +1295,7 @@ exports[`compiler: parse > Errors > EOF_IN_TAG > <div></div 1`] = `
exports[`compiler: parse > Errors > EOF_IN_TAG > <template><div 1`] = ` exports[`compiler: parse > Errors > EOF_IN_TAG > <template><div 1`] = `
{ {
"cached": 0, "cached": [],
"children": [ "children": [
{ {
"children": [], "children": [],
@ -1347,7 +1347,7 @@ exports[`compiler: parse > Errors > EOF_IN_TAG > <template><div 1`] = `
exports[`compiler: parse > Errors > EOF_IN_TAG > <template><div 1`] = ` exports[`compiler: parse > Errors > EOF_IN_TAG > <template><div 1`] = `
{ {
"cached": 0, "cached": [],
"children": [ "children": [
{ {
"children": [], "children": [],
@ -1399,7 +1399,7 @@ exports[`compiler: parse > Errors > EOF_IN_TAG > <template><div 1`] = `
exports[`compiler: parse > Errors > EOF_IN_TAG > <template><div id 1`] = ` exports[`compiler: parse > Errors > EOF_IN_TAG > <template><div id 1`] = `
{ {
"cached": 0, "cached": [],
"children": [ "children": [
{ {
"children": [], "children": [],
@ -1451,7 +1451,7 @@ exports[`compiler: parse > Errors > EOF_IN_TAG > <template><div id 1`] = `
exports[`compiler: parse > Errors > EOF_IN_TAG > <template><div id = 1`] = ` exports[`compiler: parse > Errors > EOF_IN_TAG > <template><div id = 1`] = `
{ {
"cached": 0, "cached": [],
"children": [ "children": [
{ {
"children": [], "children": [],
@ -1503,7 +1503,7 @@ exports[`compiler: parse > Errors > EOF_IN_TAG > <template><div id = 1`] = `
exports[`compiler: parse > Errors > EOF_IN_TAG > <template><div id 1`] = ` exports[`compiler: parse > Errors > EOF_IN_TAG > <template><div id 1`] = `
{ {
"cached": 0, "cached": [],
"children": [ "children": [
{ {
"children": [], "children": [],
@ -1555,7 +1555,7 @@ exports[`compiler: parse > Errors > EOF_IN_TAG > <template><div id 1`] = `
exports[`compiler: parse > Errors > EOF_IN_TAG > <template><div id="abc 1`] = ` exports[`compiler: parse > Errors > EOF_IN_TAG > <template><div id="abc 1`] = `
{ {
"cached": 0, "cached": [],
"children": [ "children": [
{ {
"children": [], "children": [],
@ -1607,7 +1607,7 @@ exports[`compiler: parse > Errors > EOF_IN_TAG > <template><div id="abc 1`] = `
exports[`compiler: parse > Errors > EOF_IN_TAG > <template><div id="abc" 1`] = ` exports[`compiler: parse > Errors > EOF_IN_TAG > <template><div id="abc" 1`] = `
{ {
"cached": 0, "cached": [],
"children": [ "children": [
{ {
"children": [], "children": [],
@ -1659,7 +1659,7 @@ exports[`compiler: parse > Errors > EOF_IN_TAG > <template><div id="abc" 1`] = `
exports[`compiler: parse > Errors > EOF_IN_TAG > <template><div id="abc"/ 1`] = ` exports[`compiler: parse > Errors > EOF_IN_TAG > <template><div id="abc"/ 1`] = `
{ {
"cached": 0, "cached": [],
"children": [ "children": [
{ {
"children": [ "children": [
@ -1729,7 +1729,7 @@ exports[`compiler: parse > Errors > EOF_IN_TAG > <template><div id="abc"/ 1`] =
exports[`compiler: parse > Errors > EOF_IN_TAG > <template><div id='abc 1`] = ` exports[`compiler: parse > Errors > EOF_IN_TAG > <template><div id='abc 1`] = `
{ {
"cached": 0, "cached": [],
"children": [ "children": [
{ {
"children": [], "children": [],
@ -1781,7 +1781,7 @@ exports[`compiler: parse > Errors > EOF_IN_TAG > <template><div id='abc 1`] = `
exports[`compiler: parse > Errors > EOF_IN_TAG > <template><div id='abc' 1`] = ` exports[`compiler: parse > Errors > EOF_IN_TAG > <template><div id='abc' 1`] = `
{ {
"cached": 0, "cached": [],
"children": [ "children": [
{ {
"children": [], "children": [],
@ -1833,7 +1833,7 @@ exports[`compiler: parse > Errors > EOF_IN_TAG > <template><div id='abc' 1`] = `
exports[`compiler: parse > Errors > EOF_IN_TAG > <template><div id='abc'/ 1`] = ` exports[`compiler: parse > Errors > EOF_IN_TAG > <template><div id='abc'/ 1`] = `
{ {
"cached": 0, "cached": [],
"children": [ "children": [
{ {
"children": [ "children": [
@ -1903,7 +1903,7 @@ exports[`compiler: parse > Errors > EOF_IN_TAG > <template><div id='abc'/ 1`] =
exports[`compiler: parse > Errors > EOF_IN_TAG > <template><div id=abc / 1`] = ` exports[`compiler: parse > Errors > EOF_IN_TAG > <template><div id=abc / 1`] = `
{ {
"cached": 0, "cached": [],
"children": [ "children": [
{ {
"children": [ "children": [
@ -1973,7 +1973,7 @@ exports[`compiler: parse > Errors > EOF_IN_TAG > <template><div id=abc / 1`] = `
exports[`compiler: parse > Errors > EOF_IN_TAG > <template><div id=abc 1`] = ` exports[`compiler: parse > Errors > EOF_IN_TAG > <template><div id=abc 1`] = `
{ {
"cached": 0, "cached": [],
"children": [ "children": [
{ {
"children": [], "children": [],
@ -2025,7 +2025,7 @@ exports[`compiler: parse > Errors > EOF_IN_TAG > <template><div id=abc 1`] = `
exports[`compiler: parse > Errors > MISSING_ATTRIBUTE_VALUE > <template><div id= /></div></template> 1`] = ` exports[`compiler: parse > Errors > MISSING_ATTRIBUTE_VALUE > <template><div id= /></div></template> 1`] = `
{ {
"cached": 0, "cached": [],
"children": [ "children": [
{ {
"children": [ "children": [
@ -2148,7 +2148,7 @@ exports[`compiler: parse > Errors > MISSING_ATTRIBUTE_VALUE > <template><div id=
exports[`compiler: parse > Errors > MISSING_ATTRIBUTE_VALUE > <template><div id= ></div></template> 1`] = ` exports[`compiler: parse > Errors > MISSING_ATTRIBUTE_VALUE > <template><div id= ></div></template> 1`] = `
{ {
"cached": 0, "cached": [],
"children": [ "children": [
{ {
"children": [ "children": [
@ -2271,7 +2271,7 @@ exports[`compiler: parse > Errors > MISSING_ATTRIBUTE_VALUE > <template><div id=
exports[`compiler: parse > Errors > MISSING_ATTRIBUTE_VALUE > <template><div id=></div></template> 1`] = ` exports[`compiler: parse > Errors > MISSING_ATTRIBUTE_VALUE > <template><div id=></div></template> 1`] = `
{ {
"cached": 0, "cached": [],
"children": [ "children": [
{ {
"children": [ "children": [
@ -2394,7 +2394,7 @@ exports[`compiler: parse > Errors > MISSING_ATTRIBUTE_VALUE > <template><div id=
exports[`compiler: parse > Errors > MISSING_END_TAG_NAME > <template></></template> 1`] = ` exports[`compiler: parse > Errors > MISSING_END_TAG_NAME > <template></></template> 1`] = `
{ {
"cached": 0, "cached": [],
"children": [ "children": [
{ {
"children": [], "children": [],
@ -2446,7 +2446,7 @@ exports[`compiler: parse > Errors > MISSING_END_TAG_NAME > <template></></templa
exports[`compiler: parse > Errors > UNEXPECTED_CHARACTER_IN_ATTRIBUTE_NAME > <template><div a"bc=''></div></template> 1`] = ` exports[`compiler: parse > Errors > UNEXPECTED_CHARACTER_IN_ATTRIBUTE_NAME > <template><div a"bc=''></div></template> 1`] = `
{ {
"cached": 0, "cached": [],
"children": [ "children": [
{ {
"children": [ "children": [
@ -2569,7 +2569,7 @@ exports[`compiler: parse > Errors > UNEXPECTED_CHARACTER_IN_ATTRIBUTE_NAME > <te
exports[`compiler: parse > Errors > UNEXPECTED_CHARACTER_IN_ATTRIBUTE_NAME > <template><div a'bc=''></div></template> 1`] = ` exports[`compiler: parse > Errors > UNEXPECTED_CHARACTER_IN_ATTRIBUTE_NAME > <template><div a'bc=''></div></template> 1`] = `
{ {
"cached": 0, "cached": [],
"children": [ "children": [
{ {
"children": [ "children": [
@ -2692,7 +2692,7 @@ exports[`compiler: parse > Errors > UNEXPECTED_CHARACTER_IN_ATTRIBUTE_NAME > <te
exports[`compiler: parse > Errors > UNEXPECTED_CHARACTER_IN_ATTRIBUTE_NAME > <template><div a<bc=''></div></template> 1`] = ` exports[`compiler: parse > Errors > UNEXPECTED_CHARACTER_IN_ATTRIBUTE_NAME > <template><div a<bc=''></div></template> 1`] = `
{ {
"cached": 0, "cached": [],
"children": [ "children": [
{ {
"children": [ "children": [
@ -2815,7 +2815,7 @@ exports[`compiler: parse > Errors > UNEXPECTED_CHARACTER_IN_ATTRIBUTE_NAME > <te
exports[`compiler: parse > Errors > UNEXPECTED_CHARACTER_IN_UNQUOTED_ATTRIBUTE_VALUE > <template><div foo=bar"></div></template> 1`] = ` exports[`compiler: parse > Errors > UNEXPECTED_CHARACTER_IN_UNQUOTED_ATTRIBUTE_VALUE > <template><div foo=bar"></div></template> 1`] = `
{ {
"cached": 0, "cached": [],
"children": [ "children": [
{ {
"children": [ "children": [
@ -2938,7 +2938,7 @@ exports[`compiler: parse > Errors > UNEXPECTED_CHARACTER_IN_UNQUOTED_ATTRIBUTE_V
exports[`compiler: parse > Errors > UNEXPECTED_CHARACTER_IN_UNQUOTED_ATTRIBUTE_VALUE > <template><div foo=bar'></div></template> 1`] = ` exports[`compiler: parse > Errors > UNEXPECTED_CHARACTER_IN_UNQUOTED_ATTRIBUTE_VALUE > <template><div foo=bar'></div></template> 1`] = `
{ {
"cached": 0, "cached": [],
"children": [ "children": [
{ {
"children": [ "children": [
@ -3061,7 +3061,7 @@ exports[`compiler: parse > Errors > UNEXPECTED_CHARACTER_IN_UNQUOTED_ATTRIBUTE_V
exports[`compiler: parse > Errors > UNEXPECTED_CHARACTER_IN_UNQUOTED_ATTRIBUTE_VALUE > <template><div foo=bar<div></div></template> 1`] = ` exports[`compiler: parse > Errors > UNEXPECTED_CHARACTER_IN_UNQUOTED_ATTRIBUTE_VALUE > <template><div foo=bar<div></div></template> 1`] = `
{ {
"cached": 0, "cached": [],
"children": [ "children": [
{ {
"children": [ "children": [
@ -3184,7 +3184,7 @@ exports[`compiler: parse > Errors > UNEXPECTED_CHARACTER_IN_UNQUOTED_ATTRIBUTE_V
exports[`compiler: parse > Errors > UNEXPECTED_CHARACTER_IN_UNQUOTED_ATTRIBUTE_VALUE > <template><div foo=bar=baz></div></template> 1`] = ` exports[`compiler: parse > Errors > UNEXPECTED_CHARACTER_IN_UNQUOTED_ATTRIBUTE_VALUE > <template><div foo=bar=baz></div></template> 1`] = `
{ {
"cached": 0, "cached": [],
"children": [ "children": [
{ {
"children": [ "children": [
@ -3307,7 +3307,7 @@ exports[`compiler: parse > Errors > UNEXPECTED_CHARACTER_IN_UNQUOTED_ATTRIBUTE_V
exports[`compiler: parse > Errors > UNEXPECTED_CHARACTER_IN_UNQUOTED_ATTRIBUTE_VALUE > <template><div foo=bar\`></div></template> 1`] = ` exports[`compiler: parse > Errors > UNEXPECTED_CHARACTER_IN_UNQUOTED_ATTRIBUTE_VALUE > <template><div foo=bar\`></div></template> 1`] = `
{ {
"cached": 0, "cached": [],
"children": [ "children": [
{ {
"children": [ "children": [
@ -3430,7 +3430,7 @@ exports[`compiler: parse > Errors > UNEXPECTED_CHARACTER_IN_UNQUOTED_ATTRIBUTE_V
exports[`compiler: parse > Errors > UNEXPECTED_EQUALS_SIGN_BEFORE_ATTRIBUTE_NAME > <template><div =></div></template> 1`] = ` exports[`compiler: parse > Errors > UNEXPECTED_EQUALS_SIGN_BEFORE_ATTRIBUTE_NAME > <template><div =></div></template> 1`] = `
{ {
"cached": 0, "cached": [],
"children": [ "children": [
{ {
"children": [ "children": [
@ -3537,7 +3537,7 @@ exports[`compiler: parse > Errors > UNEXPECTED_EQUALS_SIGN_BEFORE_ATTRIBUTE_NAME
exports[`compiler: parse > Errors > UNEXPECTED_EQUALS_SIGN_BEFORE_ATTRIBUTE_NAME > <template><div =foo=bar></div></template> 1`] = ` exports[`compiler: parse > Errors > UNEXPECTED_EQUALS_SIGN_BEFORE_ATTRIBUTE_NAME > <template><div =foo=bar></div></template> 1`] = `
{ {
"cached": 0, "cached": [],
"children": [ "children": [
{ {
"children": [ "children": [
@ -3660,7 +3660,7 @@ exports[`compiler: parse > Errors > UNEXPECTED_EQUALS_SIGN_BEFORE_ATTRIBUTE_NAME
exports[`compiler: parse > Errors > UNEXPECTED_QUESTION_MARK_INSTEAD_OF_TAG_NAME > <template><?xml?></template> 1`] = ` exports[`compiler: parse > Errors > UNEXPECTED_QUESTION_MARK_INSTEAD_OF_TAG_NAME > <template><?xml?></template> 1`] = `
{ {
"cached": 0, "cached": [],
"children": [ "children": [
{ {
"children": [], "children": [],
@ -3712,7 +3712,7 @@ exports[`compiler: parse > Errors > UNEXPECTED_QUESTION_MARK_INSTEAD_OF_TAG_NAME
exports[`compiler: parse > Errors > UNEXPECTED_SOLIDUS_IN_TAG > <template><div a/b></div></template> 1`] = ` exports[`compiler: parse > Errors > UNEXPECTED_SOLIDUS_IN_TAG > <template><div a/b></div></template> 1`] = `
{ {
"cached": 0, "cached": [],
"children": [ "children": [
{ {
"children": [ "children": [
@ -3850,7 +3850,7 @@ exports[`compiler: parse > Errors > UNEXPECTED_SOLIDUS_IN_TAG > <template><div a
exports[`compiler: parse > Errors > X_INVALID_END_TAG > <svg><![CDATA[</div>]]></svg> 1`] = ` exports[`compiler: parse > Errors > X_INVALID_END_TAG > <svg><![CDATA[</div>]]></svg> 1`] = `
{ {
"cached": 0, "cached": [],
"children": [ "children": [
{ {
"children": [ "children": [
@ -3920,7 +3920,7 @@ exports[`compiler: parse > Errors > X_INVALID_END_TAG > <svg><![CDATA[</div>]]><
exports[`compiler: parse > Errors > X_INVALID_END_TAG > <svg><!--</div>--></svg> 1`] = ` exports[`compiler: parse > Errors > X_INVALID_END_TAG > <svg><!--</div>--></svg> 1`] = `
{ {
"cached": 0, "cached": [],
"children": [ "children": [
{ {
"children": [ "children": [
@ -3990,7 +3990,7 @@ exports[`compiler: parse > Errors > X_INVALID_END_TAG > <svg><!--</div>--></svg>
exports[`compiler: parse > Errors > X_INVALID_END_TAG > <template></div></div></template> 1`] = ` exports[`compiler: parse > Errors > X_INVALID_END_TAG > <template></div></div></template> 1`] = `
{ {
"cached": 0, "cached": [],
"children": [ "children": [
{ {
"children": [], "children": [],
@ -4042,7 +4042,7 @@ exports[`compiler: parse > Errors > X_INVALID_END_TAG > <template></div></div></
exports[`compiler: parse > Errors > X_INVALID_END_TAG > <template></div></template> 1`] = ` exports[`compiler: parse > Errors > X_INVALID_END_TAG > <template></div></template> 1`] = `
{ {
"cached": 0, "cached": [],
"children": [ "children": [
{ {
"children": [], "children": [],
@ -4094,7 +4094,7 @@ exports[`compiler: parse > Errors > X_INVALID_END_TAG > <template></div></templa
exports[`compiler: parse > Errors > X_INVALID_END_TAG > <template>{{'</div>'}}</template> 1`] = ` exports[`compiler: parse > Errors > X_INVALID_END_TAG > <template>{{'</div>'}}</template> 1`] = `
{ {
"cached": 0, "cached": [],
"children": [ "children": [
{ {
"children": [ "children": [
@ -4182,7 +4182,7 @@ exports[`compiler: parse > Errors > X_INVALID_END_TAG > <template>{{'</div>'}}</
exports[`compiler: parse > Errors > X_INVALID_END_TAG > <template>a </ b</template> 1`] = ` exports[`compiler: parse > Errors > X_INVALID_END_TAG > <template>a </ b</template> 1`] = `
{ {
"cached": 0, "cached": [],
"children": [ "children": [
{ {
"children": [ "children": [
@ -4252,7 +4252,7 @@ exports[`compiler: parse > Errors > X_INVALID_END_TAG > <template>a </ b</templa
exports[`compiler: parse > Errors > X_INVALID_END_TAG > <textarea></div></textarea> 1`] = ` exports[`compiler: parse > Errors > X_INVALID_END_TAG > <textarea></div></textarea> 1`] = `
{ {
"cached": 0, "cached": [],
"children": [ "children": [
{ {
"children": [ "children": [
@ -4322,7 +4322,7 @@ exports[`compiler: parse > Errors > X_INVALID_END_TAG > <textarea></div></textar
exports[`compiler: parse > Errors > X_MISSING_DYNAMIC_DIRECTIVE_ARGUMENT_END > <div v-foo:[sef fsef] /> 1`] = ` exports[`compiler: parse > Errors > X_MISSING_DYNAMIC_DIRECTIVE_ARGUMENT_END > <div v-foo:[sef fsef] /> 1`] = `
{ {
"cached": 0, "cached": [],
"children": [ "children": [
{ {
"children": [], "children": [],
@ -4446,7 +4446,7 @@ exports[`compiler: parse > Errors > X_MISSING_DYNAMIC_DIRECTIVE_ARGUMENT_END > <
exports[`compiler: parse > Errors > X_MISSING_END_TAG > <template><div> 1`] = ` exports[`compiler: parse > Errors > X_MISSING_END_TAG > <template><div> 1`] = `
{ {
"cached": 0, "cached": [],
"children": [ "children": [
{ {
"children": [ "children": [
@ -4521,7 +4521,7 @@ exports[`compiler: parse > Errors > X_MISSING_END_TAG > <template><div> 1`] = `
exports[`compiler: parse > Errors > X_MISSING_END_TAG > <template><div></template> 1`] = ` exports[`compiler: parse > Errors > X_MISSING_END_TAG > <template><div></template> 1`] = `
{ {
"cached": 0, "cached": [],
"children": [ "children": [
{ {
"children": [ "children": [
@ -4596,7 +4596,7 @@ exports[`compiler: parse > Errors > X_MISSING_END_TAG > <template><div></templat
exports[`compiler: parse > Errors > X_MISSING_INTERPOLATION_END > <div>{{ foo</div> 1`] = ` exports[`compiler: parse > Errors > X_MISSING_INTERPOLATION_END > <div>{{ foo</div> 1`] = `
{ {
"cached": 0, "cached": [],
"children": [ "children": [
{ {
"children": [ "children": [
@ -4666,7 +4666,7 @@ exports[`compiler: parse > Errors > X_MISSING_INTERPOLATION_END > <div>{{ foo</d
exports[`compiler: parse > Errors > X_MISSING_INTERPOLATION_END > {{ 1`] = ` exports[`compiler: parse > Errors > X_MISSING_INTERPOLATION_END > {{ 1`] = `
{ {
"cached": 0, "cached": [],
"children": [ "children": [
{ {
"content": "{{", "content": "{{",
@ -4713,7 +4713,7 @@ exports[`compiler: parse > Errors > X_MISSING_INTERPOLATION_END > {{ 1`] = `
exports[`compiler: parse > Errors > X_MISSING_INTERPOLATION_END > {{ foo 1`] = ` exports[`compiler: parse > Errors > X_MISSING_INTERPOLATION_END > {{ foo 1`] = `
{ {
"cached": 0, "cached": [],
"children": [ "children": [
{ {
"content": "{{ foo", "content": "{{ foo",
@ -4760,7 +4760,7 @@ exports[`compiler: parse > Errors > X_MISSING_INTERPOLATION_END > {{ foo 1`] = `
exports[`compiler: parse > Errors > X_MISSING_INTERPOLATION_END > {{}} 1`] = ` exports[`compiler: parse > Errors > X_MISSING_INTERPOLATION_END > {{}} 1`] = `
{ {
"cached": 0, "cached": [],
"children": [ "children": [
{ {
"content": { "content": {

View File

@ -1,21 +1,5 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`scopeId compiler support > should push scopeId for hoisted nodes 1`] = `
"import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString, createTextVNode as _createTextVNode, openBlock as _openBlock, createElementBlock as _createElementBlock, pushScopeId as _pushScopeId, popScopeId as _popScopeId } from "vue"
const _withScopeId = n => (_pushScopeId("test"),n=n(),_popScopeId(),n)
const _hoisted_1 = /*#__PURE__*/ _withScopeId(() => /*#__PURE__*/_createElementVNode("div", null, "hello", -1 /* HOISTED */))
const _hoisted_2 = /*#__PURE__*/ _withScopeId(() => /*#__PURE__*/_createElementVNode("div", null, "world", -1 /* HOISTED */))
export function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, [
_hoisted_1,
_createTextVNode(_toDisplayString(_ctx.foo), 1 /* TEXT */),
_hoisted_2
]))
}"
`;
exports[`scopeId compiler support > should wrap default slot 1`] = ` exports[`scopeId compiler support > should wrap default slot 1`] = `
"import { createElementVNode as _createElementVNode, resolveComponent as _resolveComponent, withCtx as _withCtx, openBlock as _openBlock, createBlock as _createBlock } from "vue" "import { createElementVNode as _createElementVNode, resolveComponent as _resolveComponent, withCtx as _withCtx, openBlock as _openBlock, createBlock as _createBlock } from "vue"

View File

@ -47,7 +47,7 @@ function createRoot(options: Partial<RootNode> = {}): RootNode {
directives: [], directives: [],
imports: [], imports: [],
hoists: [], hoists: [],
cached: 0, cached: [],
temps: 0, temps: 0,
codegenNode: createSimpleExpression(`null`, false), codegenNode: createSimpleExpression(`null`, false),
loc: locStub, loc: locStub,
@ -422,7 +422,7 @@ describe('compiler: codegen', () => {
test('CacheExpression', () => { test('CacheExpression', () => {
const { code } = generate( const { code } = generate(
createRoot({ createRoot({
cached: 1, cached: [],
codegenNode: createCacheExpression( codegenNode: createCacheExpression(
1, 1,
createSimpleExpression(`foo`, false), createSimpleExpression(`foo`, false),
@ -440,7 +440,7 @@ describe('compiler: codegen', () => {
test('CacheExpression w/ isVNode: true', () => { test('CacheExpression w/ isVNode: true', () => {
const { code } = generate( const { code } = generate(
createRoot({ createRoot({
cached: 1, cached: [],
codegenNode: createCacheExpression( codegenNode: createCacheExpression(
1, 1,
createSimpleExpression(`foo`, false), createSimpleExpression(`foo`, false),

View File

@ -1,7 +1,4 @@
import { baseCompile } from '../src/compile' import { baseCompile } from '../src/compile'
import { POP_SCOPE_ID, PUSH_SCOPE_ID } from '../src/runtimeHelpers'
import { PatchFlags } from '@vue/shared'
import { genFlagText } from './testUtils'
/** /**
* Ensure all slot functions are wrapped with _withCtx * Ensure all slot functions are wrapped with _withCtx
@ -57,28 +54,4 @@ describe('scopeId compiler support', () => {
expect(code).toMatch(/name: i,\s+fn: _withCtx\(/) expect(code).toMatch(/name: i,\s+fn: _withCtx\(/)
expect(code).toMatchSnapshot() expect(code).toMatchSnapshot()
}) })
test('should push scopeId for hoisted nodes', () => {
const { ast, code } = baseCompile(
`<div><div>hello</div>{{ foo }}<div>world</div></div>`,
{
mode: 'module',
scopeId: 'test',
hoistStatic: true,
},
)
expect(ast.helpers).toContain(PUSH_SCOPE_ID)
expect(ast.helpers).toContain(POP_SCOPE_ID)
expect(ast.hoists.length).toBe(2)
;[
`const _withScopeId = n => (_pushScopeId("test"),n=n(),_popScopeId(),n)`,
`const _hoisted_1 = /*#__PURE__*/ _withScopeId(() => /*#__PURE__*/_createElementVNode("div", null, "hello", ${genFlagText(
PatchFlags.HOISTED,
)}))`,
`const _hoisted_2 = /*#__PURE__*/ _withScopeId(() => /*#__PURE__*/_createElementVNode("div", null, "world", ${genFlagText(
PatchFlags.HOISTED,
)}))`,
].forEach(c => expect(code).toMatch(c))
expect(code).toMatchSnapshot()
})
}) })

View File

@ -1,103 +1,87 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`compiler: hoistStatic transform > hoist element with static key 1`] = ` exports[`compiler: cacheStatic transform > cache element with static key 1`] = `
"const _Vue = Vue "const _Vue = Vue
const { createElementVNode: _createElementVNode } = _Vue
const _hoisted_1 = /*#__PURE__*/_createElementVNode("div", { key: "foo" }, null, -1 /* HOISTED */)
const _hoisted_2 = [
_hoisted_1
]
return function render(_ctx, _cache) { return function render(_ctx, _cache) {
with (_ctx) { with (_ctx) {
const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return (_openBlock(), _createElementBlock("div", null, _hoisted_2)) return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
_createElementVNode("div", { key: "foo" }, null, -1 /* CACHED */)
])))
} }
}" }"
`; `;
exports[`compiler: hoistStatic transform > hoist nested static tree 1`] = ` exports[`compiler: cacheStatic transform > cache nested children array 1`] = `
"const _Vue = Vue "const _Vue = Vue
const { createElementVNode: _createElementVNode } = _Vue
const _hoisted_1 = /*#__PURE__*/_createElementVNode("p", null, [
/*#__PURE__*/_createElementVNode("span"),
/*#__PURE__*/_createElementVNode("span")
], -1 /* HOISTED */)
const _hoisted_2 = [
_hoisted_1
]
return function render(_ctx, _cache) { return function render(_ctx, _cache) {
with (_ctx) { with (_ctx) {
const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return (_openBlock(), _createElementBlock("div", null, _hoisted_2)) return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
_createElementVNode("p", null, [
_createElementVNode("span"),
_createElementVNode("span")
], -1 /* CACHED */),
_createElementVNode("p", null, [
_createElementVNode("span"),
_createElementVNode("span")
], -1 /* CACHED */)
])))
} }
}" }"
`; `;
exports[`compiler: hoistStatic transform > hoist nested static tree with comments 1`] = ` exports[`compiler: cacheStatic transform > cache nested static tree with comments 1`] = `
"const _Vue = Vue "const _Vue = Vue
const { createElementVNode: _createElementVNode, createCommentVNode: _createCommentVNode } = _Vue
const _hoisted_1 = /*#__PURE__*/_createElementVNode("div", null, [
/*#__PURE__*/_createCommentVNode("comment")
], -1 /* HOISTED */)
const _hoisted_2 = [
_hoisted_1
]
return function render(_ctx, _cache) { return function render(_ctx, _cache) {
with (_ctx) { with (_ctx) {
const { createCommentVNode: _createCommentVNode, createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue const { createCommentVNode: _createCommentVNode, createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return (_openBlock(), _createElementBlock("div", null, _hoisted_2)) return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
_createElementVNode("div", null, [
_createCommentVNode("comment")
], -1 /* CACHED */)
])))
} }
}" }"
`; `;
exports[`compiler: hoistStatic transform > hoist siblings with common non-hoistable parent 1`] = ` exports[`compiler: cacheStatic transform > cache siblings including text with common non-hoistable parent 1`] = `
"const _Vue = Vue "const _Vue = Vue
const { createElementVNode: _createElementVNode } = _Vue
const _hoisted_1 = /*#__PURE__*/_createElementVNode("span", null, null, -1 /* HOISTED */) return function render(_ctx, _cache) {
const _hoisted_2 = /*#__PURE__*/_createElementVNode("div", null, null, -1 /* HOISTED */) with (_ctx) {
const _hoisted_3 = [ const { createElementVNode: _createElementVNode, createTextVNode: _createTextVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
_hoisted_1,
_hoisted_2 return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
] _createElementVNode("span", null, null, -1 /* CACHED */),
_createTextVNode("foo"),
_createElementVNode("div", null, null, -1 /* CACHED */)
])))
}
}"
`;
exports[`compiler: cacheStatic transform > cache single children array 1`] = `
"const _Vue = Vue
return function render(_ctx, _cache) { return function render(_ctx, _cache) {
with (_ctx) { with (_ctx) {
const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return (_openBlock(), _createElementBlock("div", null, _hoisted_3)) return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
_createElementVNode("span", { class: "inline" }, "hello", -1 /* CACHED */)
])))
} }
}" }"
`; `;
exports[`compiler: hoistStatic transform > hoist simple element 1`] = ` exports[`compiler: cacheStatic transform > hoist static props for elements with directives 1`] = `
"const _Vue = Vue
const { createElementVNode: _createElementVNode } = _Vue
const _hoisted_1 = /*#__PURE__*/_createElementVNode("span", { class: "inline" }, "hello", -1 /* HOISTED */)
const _hoisted_2 = [
_hoisted_1
]
return function render(_ctx, _cache) {
with (_ctx) {
const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return (_openBlock(), _createElementBlock("div", null, _hoisted_2))
}
}"
`;
exports[`compiler: hoistStatic transform > hoist static props for elements with directives 1`] = `
"const _Vue = Vue "const _Vue = Vue
const { createElementVNode: _createElementVNode } = _Vue const { createElementVNode: _createElementVNode } = _Vue
@ -118,7 +102,7 @@ return function render(_ctx, _cache) {
}" }"
`; `;
exports[`compiler: hoistStatic transform > hoist static props for elements with dynamic text children 1`] = ` exports[`compiler: cacheStatic transform > hoist static props for elements with dynamic text children 1`] = `
"const _Vue = Vue "const _Vue = Vue
const { createElementVNode: _createElementVNode } = _Vue const { createElementVNode: _createElementVNode } = _Vue
@ -135,7 +119,7 @@ return function render(_ctx, _cache) {
}" }"
`; `;
exports[`compiler: hoistStatic transform > hoist static props for elements with unhoistable children 1`] = ` exports[`compiler: cacheStatic transform > hoist static props for elements with unhoistable children 1`] = `
"const _Vue = Vue "const _Vue = Vue
const { createVNode: _createVNode, createElementVNode: _createElementVNode } = _Vue const { createVNode: _createVNode, createElementVNode: _createElementVNode } = _Vue
@ -156,7 +140,53 @@ return function render(_ctx, _cache) {
}" }"
`; `;
exports[`compiler: hoistStatic transform > prefixIdentifiers > hoist class with static object value 1`] = ` exports[`compiler: cacheStatic transform > prefixIdentifiers > cache nested static tree with static interpolation 1`] = `
"const _Vue = Vue
return function render(_ctx, _cache) {
with (_ctx) {
const { toDisplayString: _toDisplayString, createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
_createElementVNode("span", null, "foo " + _toDisplayString(1) + " " + _toDisplayString(true), -1 /* CACHED */)
])))
}
}"
`;
exports[`compiler: cacheStatic transform > prefixIdentifiers > cache nested static tree with static prop value 1`] = `
"const _Vue = Vue
return function render(_ctx, _cache) {
with (_ctx) {
const { toDisplayString: _toDisplayString, createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
_createElementVNode("span", { foo: 0 }, _toDisplayString(1), -1 /* CACHED */)
])))
}
}"
`;
exports[`compiler: cacheStatic transform > prefixIdentifiers > clone hoisted array children in v-for + HMR mode 1`] = `
"const _Vue = Vue
return function render(_ctx, _cache) {
with (_ctx) {
const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock, createElementVNode: _createElementVNode } = _Vue
return (_openBlock(), _createElementBlock("div", null, [
(_openBlock(true), _createElementBlock(_Fragment, null, _renderList(1, (i) => {
return (_openBlock(), _createElementBlock("div", null, [...(_cache[0] || (_cache[0] = [
_createElementVNode("span", { class: "hi" }, null, -1 /* CACHED */)
]))]))
}), 256 /* UNKEYED_FRAGMENT */))
]))
}
}"
`;
exports[`compiler: cacheStatic transform > prefixIdentifiers > hoist class with static object value 1`] = `
"const _Vue = Vue "const _Vue = Vue
const { createElementVNode: _createElementVNode } = _Vue const { createElementVNode: _createElementVNode } = _Vue
@ -175,50 +205,8 @@ return function render(_ctx, _cache) {
}" }"
`; `;
exports[`compiler: hoistStatic transform > prefixIdentifiers > hoist nested static tree with static interpolation 1`] = ` exports[`compiler: cacheStatic transform > prefixIdentifiers > should NOT cache SVG with directives 1`] = `
"const _Vue = Vue "const _Vue = Vue
const { createElementVNode: _createElementVNode } = _Vue
const _hoisted_1 = /*#__PURE__*/_createElementVNode("span", null, "foo " + /*#__PURE__*/_toDisplayString(1) + " " + /*#__PURE__*/_toDisplayString(true), -1 /* HOISTED */)
const _hoisted_2 = [
_hoisted_1
]
return function render(_ctx, _cache) {
with (_ctx) {
const { toDisplayString: _toDisplayString, createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return (_openBlock(), _createElementBlock("div", null, _hoisted_2))
}
}"
`;
exports[`compiler: hoistStatic transform > prefixIdentifiers > hoist nested static tree with static prop value 1`] = `
"const _Vue = Vue
const { createElementVNode: _createElementVNode } = _Vue
const _hoisted_1 = /*#__PURE__*/_createElementVNode("span", { foo: 0 }, /*#__PURE__*/_toDisplayString(1), -1 /* HOISTED */)
const _hoisted_2 = [
_hoisted_1
]
return function render(_ctx, _cache) {
with (_ctx) {
const { toDisplayString: _toDisplayString, createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return (_openBlock(), _createElementBlock("div", null, _hoisted_2))
}
}"
`;
exports[`compiler: hoistStatic transform > prefixIdentifiers > should NOT hoist SVG with directives 1`] = `
"const _Vue = Vue
const { createElementVNode: _createElementVNode } = _Vue
const _hoisted_1 = /*#__PURE__*/_createElementVNode("path", { d: "M2,3H5.5L12" }, null, -1 /* HOISTED */)
const _hoisted_2 = [
_hoisted_1
]
return function render(_ctx, _cache) { return function render(_ctx, _cache) {
with (_ctx) { with (_ctx) {
@ -227,7 +215,9 @@ return function render(_ctx, _cache) {
const _directive_foo = _resolveDirective("foo") const _directive_foo = _resolveDirective("foo")
return (_openBlock(), _createElementBlock("div", null, [ return (_openBlock(), _createElementBlock("div", null, [
_withDirectives((_openBlock(), _createElementBlock("svg", null, _hoisted_2)), [ _withDirectives((_openBlock(), _createElementBlock("svg", null, _cache[0] || (_cache[0] = [
_createElementVNode("path", { d: "M2,3H5.5L12" }, null, -1 /* CACHED */)
]))), [
[_directive_foo] [_directive_foo]
]) ])
])) ]))
@ -235,7 +225,7 @@ return function render(_ctx, _cache) {
}" }"
`; `;
exports[`compiler: hoistStatic transform > prefixIdentifiers > should NOT hoist elements with cached handlers + other bindings 1`] = ` exports[`compiler: cacheStatic transform > prefixIdentifiers > should NOT cache elements with cached handlers + other bindings 1`] = `
"import { normalizeClass as _normalizeClass, createElementVNode as _createElementVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue" "import { normalizeClass as _normalizeClass, createElementVNode as _createElementVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
export function render(_ctx, _cache) { export function render(_ctx, _cache) {
@ -250,7 +240,7 @@ export function render(_ctx, _cache) {
}" }"
`; `;
exports[`compiler: hoistStatic transform > prefixIdentifiers > should NOT hoist elements with cached handlers 1`] = ` exports[`compiler: cacheStatic transform > prefixIdentifiers > should NOT cache elements with cached handlers 1`] = `
"import { createElementVNode as _createElementVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue" "import { createElementVNode as _createElementVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
export function render(_ctx, _cache) { export function render(_ctx, _cache) {
@ -264,7 +254,7 @@ export function render(_ctx, _cache) {
}" }"
`; `;
exports[`compiler: hoistStatic transform > prefixIdentifiers > should NOT hoist expressions that refer scope variables (2) 1`] = ` exports[`compiler: cacheStatic transform > prefixIdentifiers > should NOT cache expressions that refer scope variables (2) 1`] = `
"const _Vue = Vue "const _Vue = Vue
return function render(_ctx, _cache) { return function render(_ctx, _cache) {
@ -282,7 +272,7 @@ return function render(_ctx, _cache) {
}" }"
`; `;
exports[`compiler: hoistStatic transform > prefixIdentifiers > should NOT hoist expressions that refer scope variables (v-slot) 1`] = ` exports[`compiler: cacheStatic transform > prefixIdentifiers > should NOT cache expressions that refer scope variables (v-slot) 1`] = `
"const _Vue = Vue "const _Vue = Vue
return function render(_ctx, _cache) { return function render(_ctx, _cache) {
@ -301,7 +291,7 @@ return function render(_ctx, _cache) {
}" }"
`; `;
exports[`compiler: hoistStatic transform > prefixIdentifiers > should NOT hoist expressions that refer scope variables 1`] = ` exports[`compiler: cacheStatic transform > prefixIdentifiers > should NOT cache expressions that refer scope variables 1`] = `
"const _Vue = Vue "const _Vue = Vue
return function render(_ctx, _cache) { return function render(_ctx, _cache) {
@ -319,7 +309,7 @@ return function render(_ctx, _cache) {
}" }"
`; `;
exports[`compiler: hoistStatic transform > prefixIdentifiers > should NOT hoist keyed template v-for with plain element child 1`] = ` exports[`compiler: cacheStatic transform > prefixIdentifiers > should NOT cache keyed template v-for with plain element child 1`] = `
"const _Vue = Vue "const _Vue = Vue
return function render(_ctx, _cache) { return function render(_ctx, _cache) {
@ -335,7 +325,7 @@ return function render(_ctx, _cache) {
}" }"
`; `;
exports[`compiler: hoistStatic transform > should NOT hoist components 1`] = ` exports[`compiler: cacheStatic transform > should NOT cache components 1`] = `
"const _Vue = Vue "const _Vue = Vue
return function render(_ctx, _cache) { return function render(_ctx, _cache) {
@ -351,7 +341,7 @@ return function render(_ctx, _cache) {
}" }"
`; `;
exports[`compiler: hoistStatic transform > should NOT hoist element with dynamic key 1`] = ` exports[`compiler: cacheStatic transform > should NOT cache element with dynamic key 1`] = `
"const _Vue = Vue "const _Vue = Vue
return function render(_ctx, _cache) { return function render(_ctx, _cache) {
@ -365,7 +355,7 @@ return function render(_ctx, _cache) {
}" }"
`; `;
exports[`compiler: hoistStatic transform > should NOT hoist element with dynamic props (but hoist the props list) 1`] = ` exports[`compiler: cacheStatic transform > should NOT cache element with dynamic props (but hoist the props list) 1`] = `
"const _Vue = Vue "const _Vue = Vue
const { createElementVNode: _createElementVNode } = _Vue const { createElementVNode: _createElementVNode } = _Vue
@ -382,7 +372,7 @@ return function render(_ctx, _cache) {
}" }"
`; `;
exports[`compiler: hoistStatic transform > should NOT hoist element with dynamic ref 1`] = ` exports[`compiler: cacheStatic transform > should NOT cache element with dynamic ref 1`] = `
"const _Vue = Vue "const _Vue = Vue
return function render(_ctx, _cache) { return function render(_ctx, _cache) {
@ -396,42 +386,7 @@ return function render(_ctx, _cache) {
}" }"
`; `;
exports[`compiler: hoistStatic transform > should NOT hoist root node 1`] = ` exports[`compiler: cacheStatic transform > should cache v-if props/children if static 1`] = `
"const _Vue = Vue
return function render(_ctx, _cache) {
with (_ctx) {
const { openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return (_openBlock(), _createElementBlock("div"))
}
}"
`;
exports[`compiler: hoistStatic transform > should hoist v-for children if static 1`] = `
"const _Vue = Vue
const { createElementVNode: _createElementVNode } = _Vue
const _hoisted_1 = { id: "foo" }
const _hoisted_2 = /*#__PURE__*/_createElementVNode("span", null, null, -1 /* HOISTED */)
const _hoisted_3 = [
_hoisted_2
]
return function render(_ctx, _cache) {
with (_ctx) {
const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock, createElementVNode: _createElementVNode } = _Vue
return (_openBlock(), _createElementBlock("div", null, [
(_openBlock(true), _createElementBlock(_Fragment, null, _renderList(list, (i) => {
return (_openBlock(), _createElementBlock("div", _hoisted_1, _hoisted_3))
}), 256 /* UNKEYED_FRAGMENT */))
]))
}
}"
`;
exports[`compiler: hoistStatic transform > should hoist v-if props/children if static 1`] = `
"const _Vue = Vue "const _Vue = Vue
const { createElementVNode: _createElementVNode, createCommentVNode: _createCommentVNode } = _Vue const { createElementVNode: _createElementVNode, createCommentVNode: _createCommentVNode } = _Vue
@ -439,10 +394,6 @@ const _hoisted_1 = {
key: 0, key: 0,
id: "foo" id: "foo"
} }
const _hoisted_2 = /*#__PURE__*/_createElementVNode("span", null, null, -1 /* HOISTED */)
const _hoisted_3 = [
_hoisted_2
]
return function render(_ctx, _cache) { return function render(_ctx, _cache) {
with (_ctx) { with (_ctx) {
@ -450,9 +401,32 @@ return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, [ return (_openBlock(), _createElementBlock("div", null, [
ok ok
? (_openBlock(), _createElementBlock("div", _hoisted_1, _hoisted_3)) ? (_openBlock(), _createElementBlock("div", _hoisted_1, _cache[0] || (_cache[0] = [
_createElementVNode("span", null, null, -1 /* CACHED */)
])))
: _createCommentVNode("v-if", true) : _createCommentVNode("v-if", true)
])) ]))
} }
}" }"
`; `;
exports[`compiler: cacheStatic transform > should hoist v-for children if static 1`] = `
"const _Vue = Vue
const { createElementVNode: _createElementVNode } = _Vue
const _hoisted_1 = { id: "foo" }
return function render(_ctx, _cache) {
with (_ctx) {
const { renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock, createElementVNode: _createElementVNode } = _Vue
return (_openBlock(), _createElementBlock("div", null, [
(_openBlock(true), _createElementBlock(_Fragment, null, _renderList(list, (i) => {
return (_openBlock(), _createElementBlock("div", _hoisted_1, _cache[0] || (_cache[0] = [
_createElementVNode("span", null, null, -1 /* CACHED */)
])))
}), 256 /* UNKEYED_FRAGMENT */))
]))
}
}"
`;

View File

@ -25,18 +25,33 @@ import { createObjectMatcher, genFlagText } from '../testUtils'
import { transformText } from '../../src/transforms/transformText' import { transformText } from '../../src/transforms/transformText'
import { PatchFlags } from '@vue/shared' import { PatchFlags } from '@vue/shared'
const hoistedChildrenArrayMatcher = (startIndex = 1, length = 1) => ({ const cachedChildrenArrayMatcher = (
tags: string[],
needArraySpread = false,
) => ({
type: NodeTypes.JS_CACHE_EXPRESSION,
needArraySpread,
value: {
type: NodeTypes.JS_ARRAY_EXPRESSION, type: NodeTypes.JS_ARRAY_EXPRESSION,
elements: new Array(length).fill(0).map((_, i) => ({ elements: tags.map(tag => {
if (tag === '') {
return {
type: NodeTypes.TEXT_CALL,
}
} else {
return {
type: NodeTypes.ELEMENT, type: NodeTypes.ELEMENT,
codegenNode: { codegenNode: {
type: NodeTypes.SIMPLE_EXPRESSION, type: NodeTypes.VNODE_CALL,
content: `_hoisted_${startIndex + i}`, tag: JSON.stringify(tag),
},
}
}
}),
}, },
})),
}) })
function transformWithHoist(template: string, options: CompilerOptions = {}) { function transformWithCache(template: string, options: CompilerOptions = {}) {
const ast = parse(template) const ast = parse(template)
transform(ast, { transform(ast, {
hoistStatic: true, hoistStatic: true,
@ -60,101 +75,253 @@ function transformWithHoist(template: string, options: CompilerOptions = {}) {
return ast return ast
} }
describe('compiler: hoistStatic transform', () => { describe('compiler: cacheStatic transform', () => {
test('should NOT hoist root node', () => { test('should NOT cache root node', () => {
// if the whole tree is static, the root still needs to be a block // if the whole tree is static, the root still needs to be a block
// so that it's patched in optimized mode to skip children // so that it's patched in optimized mode to skip children
const root = transformWithHoist(`<div/>`) const root = transformWithCache(`<div/>`)
expect(root.hoists.length).toBe(0)
expect(root.codegenNode).toMatchObject({ expect(root.codegenNode).toMatchObject({
type: NodeTypes.VNODE_CALL,
tag: `"div"`, tag: `"div"`,
}) })
expect(generate(root).code).toMatchSnapshot() expect(root.cached.length).toBe(0)
}) })
test('hoist simple element', () => { test('cache root node children', () => {
const root = transformWithHoist( // we don't have access to the root codegenNode during the transform
// so we only cache each child individually
const root = transformWithCache(
`<span class="inline">hello</span><span class="inline">hello</span>`,
)
expect(root.codegenNode).toMatchObject({
type: NodeTypes.VNODE_CALL,
children: [
{ codegenNode: { type: NodeTypes.JS_CACHE_EXPRESSION } },
{ codegenNode: { type: NodeTypes.JS_CACHE_EXPRESSION } },
],
})
expect(root.cached.length).toBe(2)
})
test('cache single children array', () => {
const root = transformWithCache(
`<div><span class="inline">hello</span></div>`, `<div><span class="inline">hello</span></div>`,
) )
expect(root.hoists).toMatchObject([
{
type: NodeTypes.VNODE_CALL,
tag: `"span"`,
props: createObjectMatcher({ class: 'inline' }),
children: {
type: NodeTypes.TEXT,
content: `hello`,
},
},
hoistedChildrenArrayMatcher(),
])
expect(root.codegenNode).toMatchObject({ expect(root.codegenNode).toMatchObject({
tag: `"div"`, tag: `"div"`,
props: undefined, props: undefined,
children: { content: `_hoisted_2` }, children: cachedChildrenArrayMatcher(['span']),
}) })
expect(root.cached.length).toBe(1)
expect(generate(root).code).toMatchSnapshot() expect(generate(root).code).toMatchSnapshot()
}) })
test('hoist nested static tree', () => { test('cache nested children array', () => {
const root = transformWithHoist(`<div><p><span/><span/></p></div>`) const root = transformWithCache(
expect(root.hoists).toMatchObject([ `<div><p><span/><span/></p><p><span/><span/></p></div>`,
)
expect((root.codegenNode as VNodeCall).children).toMatchObject(
cachedChildrenArrayMatcher(['p', 'p']),
)
expect(root.cached.length).toBe(1)
expect(generate(root).code).toMatchSnapshot()
})
test('cache nested static tree with comments', () => {
const root = transformWithCache(`<div><div><!--comment--></div></div>`)
expect((root.codegenNode as VNodeCall).children).toMatchObject(
cachedChildrenArrayMatcher(['div']),
)
expect(root.cached.length).toBe(1)
expect(generate(root).code).toMatchSnapshot()
})
test('cache siblings including text with common non-hoistable parent', () => {
const root = transformWithCache(`<div><span/>foo<div/></div>`)
expect((root.codegenNode as VNodeCall).children).toMatchObject(
cachedChildrenArrayMatcher(['span', '', 'div']),
)
expect(root.cached.length).toBe(1)
expect(generate(root).code).toMatchSnapshot()
})
test('cache inside default slot', () => {
const root = transformWithCache(`<Foo>{{x}}<span/></Foo>`)
expect((root.codegenNode as VNodeCall).children).toMatchObject({
properties: [
{ {
type: NodeTypes.VNODE_CALL, key: { content: 'default' },
tag: `"p"`, value: {
props: undefined, type: NodeTypes.JS_FUNCTION_EXPRESSION,
children: [ returns: [
{ type: NodeTypes.ELEMENT, tag: `span` }, {
{ type: NodeTypes.ELEMENT, tag: `span` }, type: NodeTypes.TEXT_CALL,
},
// first slot child cached
{
type: NodeTypes.ELEMENT,
codegenNode: {
type: NodeTypes.JS_CACHE_EXPRESSION,
},
},
], ],
}, },
hoistedChildrenArrayMatcher(),
])
expect((root.codegenNode as VNodeCall).children).toMatchObject({
content: '_hoisted_2',
})
expect(generate(root).code).toMatchSnapshot()
})
test('hoist nested static tree with comments', () => {
const root = transformWithHoist(`<div><div><!--comment--></div></div>`)
expect(root.hoists).toMatchObject([
{
type: NodeTypes.VNODE_CALL,
tag: `"div"`,
props: undefined,
children: [{ type: NodeTypes.COMMENT, content: `comment` }],
},
hoistedChildrenArrayMatcher(),
])
expect((root.codegenNode as VNodeCall).children).toMatchObject({
content: `_hoisted_2`,
})
expect(generate(root).code).toMatchSnapshot()
})
test('hoist siblings with common non-hoistable parent', () => {
const root = transformWithHoist(`<div><span/><div/></div>`)
expect(root.hoists).toMatchObject([
{
type: NodeTypes.VNODE_CALL,
tag: `"span"`,
}, },
{ {
type: NodeTypes.VNODE_CALL, /* _ slot flag */
tag: `"div"`,
}, },
hoistedChildrenArrayMatcher(1, 2), ],
])
expect((root.codegenNode as VNodeCall).children).toMatchObject({
content: '_hoisted_3',
}) })
expect(generate(root).code).toMatchSnapshot()
}) })
test('should NOT hoist components', () => { test('cache default slot as a whole', () => {
const root = transformWithHoist(`<div><Comp/></div>`) const root = transformWithCache(`<Foo><span/><span/></Foo>`)
expect(root.hoists.length).toBe(0) expect((root.codegenNode as VNodeCall).children).toMatchObject({
properties: [
{
key: { content: 'default' },
value: {
type: NodeTypes.JS_FUNCTION_EXPRESSION,
returns: {
type: NodeTypes.JS_CACHE_EXPRESSION,
value: {
type: NodeTypes.JS_ARRAY_EXPRESSION,
elements: [
{ type: NodeTypes.ELEMENT },
{ type: NodeTypes.ELEMENT },
],
},
},
},
},
{
/* _ slot flag */
},
],
})
})
test('cache inside named slot', () => {
const root = transformWithCache(
`<Foo><template #foo>{{x}}<span/></template></Foo>`,
)
expect((root.codegenNode as VNodeCall).children).toMatchObject({
properties: [
{
key: { content: 'foo' },
value: {
type: NodeTypes.JS_FUNCTION_EXPRESSION,
returns: [
{
type: NodeTypes.TEXT_CALL,
},
// first slot child cached
{
type: NodeTypes.ELEMENT,
codegenNode: {
type: NodeTypes.JS_CACHE_EXPRESSION,
},
},
],
},
},
{
/* _ slot flag */
},
],
})
})
test('cache named slot as a whole', () => {
const root = transformWithCache(
`<Foo><template #foo><span/><span/></template></Foo>`,
)
expect((root.codegenNode as VNodeCall).children).toMatchObject({
properties: [
{
key: { content: 'foo' },
value: {
type: NodeTypes.JS_FUNCTION_EXPRESSION,
returns: {
type: NodeTypes.JS_CACHE_EXPRESSION,
value: {
type: NodeTypes.JS_ARRAY_EXPRESSION,
elements: [
{ type: NodeTypes.ELEMENT },
{ type: NodeTypes.ELEMENT },
],
},
},
},
},
{
/* _ slot flag */
},
],
})
})
test('cache dynamically named slot as a whole', () => {
const root = transformWithCache(
`<Foo><template #[foo]><span/><span/></template></Foo>`,
)
expect((root.codegenNode as VNodeCall).children).toMatchObject({
properties: [
{
key: { content: 'foo', isStatic: false },
value: {
type: NodeTypes.JS_FUNCTION_EXPRESSION,
returns: {
type: NodeTypes.JS_CACHE_EXPRESSION,
value: {
type: NodeTypes.JS_ARRAY_EXPRESSION,
elements: [
{ type: NodeTypes.ELEMENT },
{ type: NodeTypes.ELEMENT },
],
},
},
},
},
{
/* _ slot flag */
},
],
})
})
test('cache dynamically named (expression) slot as a whole', () => {
const root = transformWithCache(
`<Foo><template #[foo+1]><span/><span/></template></Foo>`,
{ prefixIdentifiers: true },
)
expect((root.codegenNode as VNodeCall).children).toMatchObject({
properties: [
{
key: { type: NodeTypes.COMPOUND_EXPRESSION },
value: {
type: NodeTypes.JS_FUNCTION_EXPRESSION,
returns: {
type: NodeTypes.JS_CACHE_EXPRESSION,
value: {
type: NodeTypes.JS_ARRAY_EXPRESSION,
elements: [
{ type: NodeTypes.ELEMENT },
{ type: NodeTypes.ELEMENT },
],
},
},
},
},
{
/* _ slot flag */
},
],
})
})
test('should NOT cache components', () => {
const root = transformWithCache(`<div><Comp/></div>`)
expect((root.codegenNode as VNodeCall).children).toMatchObject([ expect((root.codegenNode as VNodeCall).children).toMatchObject([
{ {
type: NodeTypes.ELEMENT, type: NodeTypes.ELEMENT,
@ -164,11 +331,12 @@ describe('compiler: hoistStatic transform', () => {
}, },
}, },
]) ])
expect(root.cached.length).toBe(0)
expect(generate(root).code).toMatchSnapshot() expect(generate(root).code).toMatchSnapshot()
}) })
test('should NOT hoist element with dynamic props (but hoist the props list)', () => { test('should NOT cache element with dynamic props (but hoist the props list)', () => {
const root = transformWithHoist(`<div><div :id="foo"/></div>`) const root = transformWithCache(`<div><div :id="foo"/></div>`)
expect(root.hoists.length).toBe(1) expect(root.hoists.length).toBe(1)
expect((root.codegenNode as VNodeCall).children).toMatchObject([ expect((root.codegenNode as VNodeCall).children).toMatchObject([
{ {
@ -189,31 +357,23 @@ describe('compiler: hoistStatic transform', () => {
}, },
}, },
]) ])
expect(root.cached.length).toBe(0)
expect(generate(root).code).toMatchSnapshot() expect(generate(root).code).toMatchSnapshot()
}) })
test('hoist element with static key', () => { test('cache element with static key', () => {
const root = transformWithHoist(`<div><div key="foo"/></div>`) const root = transformWithCache(`<div><div key="foo"/></div>`)
expect(root.hoists.length).toBe(2)
expect(root.hoists).toMatchObject([
{
type: NodeTypes.VNODE_CALL,
tag: `"div"`,
props: createObjectMatcher({ key: 'foo' }),
},
hoistedChildrenArrayMatcher(),
])
expect(root.codegenNode).toMatchObject({ expect(root.codegenNode).toMatchObject({
tag: `"div"`, tag: `"div"`,
props: undefined, props: undefined,
children: { content: `_hoisted_2` }, children: cachedChildrenArrayMatcher(['div']),
}) })
expect(root.cached.length).toBe(1)
expect(generate(root).code).toMatchSnapshot() expect(generate(root).code).toMatchSnapshot()
}) })
test('should NOT hoist element with dynamic key', () => { test('should NOT cache element with dynamic key', () => {
const root = transformWithHoist(`<div><div :key="foo"/></div>`) const root = transformWithCache(`<div><div :key="foo"/></div>`)
expect(root.hoists.length).toBe(0)
expect((root.codegenNode as VNodeCall).children).toMatchObject([ expect((root.codegenNode as VNodeCall).children).toMatchObject([
{ {
type: NodeTypes.ELEMENT, type: NodeTypes.ELEMENT,
@ -226,12 +386,12 @@ describe('compiler: hoistStatic transform', () => {
}, },
}, },
]) ])
expect(root.cached.length).toBe(0)
expect(generate(root).code).toMatchSnapshot() expect(generate(root).code).toMatchSnapshot()
}) })
test('should NOT hoist element with dynamic ref', () => { test('should NOT cache element with dynamic ref', () => {
const root = transformWithHoist(`<div><div :ref="foo"/></div>`) const root = transformWithCache(`<div><div :ref="foo"/></div>`)
expect(root.hoists.length).toBe(0)
expect((root.codegenNode as VNodeCall).children).toMatchObject([ expect((root.codegenNode as VNodeCall).children).toMatchObject([
{ {
type: NodeTypes.ELEMENT, type: NodeTypes.ELEMENT,
@ -246,11 +406,12 @@ describe('compiler: hoistStatic transform', () => {
}, },
}, },
]) ])
expect(root.cached.length).toBe(0)
expect(generate(root).code).toMatchSnapshot() expect(generate(root).code).toMatchSnapshot()
}) })
test('hoist static props for elements with directives', () => { test('hoist static props for elements with directives', () => {
const root = transformWithHoist(`<div><div id="foo" v-foo/></div>`) const root = transformWithCache(`<div><div id="foo" v-foo/></div>`)
expect(root.hoists).toMatchObject([createObjectMatcher({ id: 'foo' })]) expect(root.hoists).toMatchObject([createObjectMatcher({ id: 'foo' })])
expect((root.codegenNode as VNodeCall).children).toMatchObject([ expect((root.codegenNode as VNodeCall).children).toMatchObject([
{ {
@ -270,11 +431,12 @@ describe('compiler: hoistStatic transform', () => {
}, },
}, },
]) ])
expect(root.cached.length).toBe(0)
expect(generate(root).code).toMatchSnapshot() expect(generate(root).code).toMatchSnapshot()
}) })
test('hoist static props for elements with dynamic text children', () => { test('hoist static props for elements with dynamic text children', () => {
const root = transformWithHoist( const root = transformWithCache(
`<div><div id="foo">{{ hello }}</div></div>`, `<div><div id="foo">{{ hello }}</div></div>`,
) )
expect(root.hoists).toMatchObject([createObjectMatcher({ id: 'foo' })]) expect(root.hoists).toMatchObject([createObjectMatcher({ id: 'foo' })])
@ -290,11 +452,12 @@ describe('compiler: hoistStatic transform', () => {
}, },
}, },
]) ])
expect(root.cached.length).toBe(0)
expect(generate(root).code).toMatchSnapshot() expect(generate(root).code).toMatchSnapshot()
}) })
test('hoist static props for elements with unhoistable children', () => { test('hoist static props for elements with unhoistable children', () => {
const root = transformWithHoist(`<div><div id="foo"><Comp/></div></div>`) const root = transformWithCache(`<div><div id="foo"><Comp/></div></div>`)
expect(root.hoists).toMatchObject([createObjectMatcher({ id: 'foo' })]) expect(root.hoists).toMatchObject([createObjectMatcher({ id: 'foo' })])
expect((root.codegenNode as VNodeCall).children).toMatchObject([ expect((root.codegenNode as VNodeCall).children).toMatchObject([
{ {
@ -307,11 +470,12 @@ describe('compiler: hoistStatic transform', () => {
}, },
}, },
]) ])
expect(root.cached.length).toBe(0)
expect(generate(root).code).toMatchSnapshot() expect(generate(root).code).toMatchSnapshot()
}) })
test('should hoist v-if props/children if static', () => { test('should cache v-if props/children if static', () => {
const root = transformWithHoist( const root = transformWithCache(
`<div><div v-if="ok" id="foo"><span/></div></div>`, `<div><div v-if="ok" id="foo"><span/></div></div>`,
) )
expect(root.hoists).toMatchObject([ expect(root.hoists).toMatchObject([
@ -319,40 +483,31 @@ describe('compiler: hoistStatic transform', () => {
key: `[0]`, // key injected by v-if branch key: `[0]`, // key injected by v-if branch
id: 'foo', id: 'foo',
}), }),
{
type: NodeTypes.VNODE_CALL,
tag: `"span"`,
},
hoistedChildrenArrayMatcher(2),
]) ])
expect( expect(
((root.children[0] as ElementNode).children[0] as IfNode).codegenNode, ((root.children[0] as ElementNode).children[0] as IfNode).codegenNode,
).toMatchObject({ ).toMatchObject({
type: NodeTypes.JS_CONDITIONAL_EXPRESSION, type: NodeTypes.JS_CONDITIONAL_EXPRESSION,
consequent: { consequent: {
// blocks should NOT be hoisted // blocks should NOT be cached
type: NodeTypes.VNODE_CALL, type: NodeTypes.VNODE_CALL,
tag: `"div"`, tag: `"div"`,
props: { content: `_hoisted_1` }, props: { content: `_hoisted_1` },
children: { content: `_hoisted_3` }, children: cachedChildrenArrayMatcher(['span']),
}, },
}) })
expect(root.cached.length).toBe(1)
expect(generate(root).code).toMatchSnapshot() expect(generate(root).code).toMatchSnapshot()
}) })
test('should hoist v-for children if static', () => { test('should hoist v-for children if static', () => {
const root = transformWithHoist( const root = transformWithCache(
`<div><div v-for="i in list" id="foo"><span/></div></div>`, `<div><div v-for="i in list" id="foo"><span/></div></div>`,
) )
expect(root.hoists).toMatchObject([ expect(root.hoists).toMatchObject([
createObjectMatcher({ createObjectMatcher({
id: 'foo', id: 'foo',
}), }),
{
type: NodeTypes.VNODE_CALL,
tag: `"span"`,
},
hoistedChildrenArrayMatcher(2),
]) ])
const forBlockCodegen = ( const forBlockCodegen = (
(root.children[0] as ElementNode).children[0] as ForNode (root.children[0] as ElementNode).children[0] as ForNode
@ -372,78 +527,47 @@ describe('compiler: hoistStatic transform', () => {
type: NodeTypes.VNODE_CALL, type: NodeTypes.VNODE_CALL,
tag: `"div"`, tag: `"div"`,
props: { content: `_hoisted_1` }, props: { content: `_hoisted_1` },
children: { content: `_hoisted_3` }, children: cachedChildrenArrayMatcher(['span']),
}) })
expect(root.cached.length).toBe(1)
expect(generate(root).code).toMatchSnapshot() expect(generate(root).code).toMatchSnapshot()
}) })
describe('prefixIdentifiers', () => { describe('prefixIdentifiers', () => {
test('hoist nested static tree with static interpolation', () => { test('cache nested static tree with static interpolation', () => {
const root = transformWithHoist( const root = transformWithCache(
`<div><span>foo {{ 1 }} {{ true }}</span></div>`, `<div><span>foo {{ 1 }} {{ true }}</span></div>`,
{ {
prefixIdentifiers: true, prefixIdentifiers: true,
}, },
) )
expect(root.hoists).toMatchObject([
{
type: NodeTypes.VNODE_CALL,
tag: `"span"`,
props: undefined,
children: {
type: NodeTypes.COMPOUND_EXPRESSION,
},
},
hoistedChildrenArrayMatcher(),
])
expect(root.codegenNode).toMatchObject({ expect(root.codegenNode).toMatchObject({
tag: `"div"`, tag: `"div"`,
props: undefined, props: undefined,
children: { children: cachedChildrenArrayMatcher(['span']),
type: NodeTypes.SIMPLE_EXPRESSION,
content: `_hoisted_2`,
},
}) })
expect(root.cached.length).toBe(1)
expect(generate(root).code).toMatchSnapshot() expect(generate(root).code).toMatchSnapshot()
}) })
test('hoist nested static tree with static prop value', () => { test('cache nested static tree with static prop value', () => {
const root = transformWithHoist( const root = transformWithCache(
`<div><span :foo="0">{{ 1 }}</span></div>`, `<div><span :foo="0">{{ 1 }}</span></div>`,
{ {
prefixIdentifiers: true, prefixIdentifiers: true,
}, },
) )
expect(root.hoists).toMatchObject([
{
type: NodeTypes.VNODE_CALL,
tag: `"span"`,
props: createObjectMatcher({ foo: `[0]` }),
children: {
type: NodeTypes.INTERPOLATION,
content: {
content: `1`,
isStatic: false,
constType: ConstantTypes.CAN_STRINGIFY,
},
},
},
hoistedChildrenArrayMatcher(),
])
expect(root.codegenNode).toMatchObject({ expect(root.codegenNode).toMatchObject({
tag: `"div"`, tag: `"div"`,
props: undefined, props: undefined,
children: { children: cachedChildrenArrayMatcher(['span']),
type: NodeTypes.SIMPLE_EXPRESSION,
content: `_hoisted_2`,
},
}) })
expect(root.cached.length).toBe(1)
expect(generate(root).code).toMatchSnapshot() expect(generate(root).code).toMatchSnapshot()
}) })
test('hoist class with static object value', () => { test('hoist class with static object value', () => {
const root = transformWithHoist( const root = transformWithCache(
`<div><span :class="{ foo: true }">{{ bar }}</span></div>`, `<div><span :class="{ foo: true }">{{ bar }}</span></div>`,
{ {
prefixIdentifiers: true, prefixIdentifiers: true,
@ -504,44 +628,44 @@ describe('compiler: hoistStatic transform', () => {
expect(generate(root).code).toMatchSnapshot() expect(generate(root).code).toMatchSnapshot()
}) })
test('should NOT hoist expressions that refer scope variables', () => { test('should NOT cache expressions that refer scope variables', () => {
const root = transformWithHoist( const root = transformWithCache(
`<div><p v-for="o in list"><span>{{ o }}</span></p></div>`, `<div><p v-for="o in list"><span>{{ o }}</span></p></div>`,
{ {
prefixIdentifiers: true, prefixIdentifiers: true,
}, },
) )
expect(root.hoists.length).toBe(0) expect(root.cached.length).toBe(0)
expect(generate(root).code).toMatchSnapshot() expect(generate(root).code).toMatchSnapshot()
}) })
test('should NOT hoist expressions that refer scope variables (2)', () => { test('should NOT cache expressions that refer scope variables (2)', () => {
const root = transformWithHoist( const root = transformWithCache(
`<div><p v-for="o in list"><span>{{ o + 'foo' }}</span></p></div>`, `<div><p v-for="o in list"><span>{{ o + 'foo' }}</span></p></div>`,
{ {
prefixIdentifiers: true, prefixIdentifiers: true,
}, },
) )
expect(root.hoists.length).toBe(0) expect(root.cached.length).toBe(0)
expect(generate(root).code).toMatchSnapshot() expect(generate(root).code).toMatchSnapshot()
}) })
test('should NOT hoist expressions that refer scope variables (v-slot)', () => { test('should NOT cache expressions that refer scope variables (v-slot)', () => {
const root = transformWithHoist( const root = transformWithCache(
`<Comp v-slot="{ foo }">{{ foo }}</Comp>`, `<Comp v-slot="{ foo }">{{ foo }}</Comp>`,
{ {
prefixIdentifiers: true, prefixIdentifiers: true,
}, },
) )
expect(root.hoists.length).toBe(0) expect(root.cached.length).toBe(0)
expect(generate(root).code).toMatchSnapshot() expect(generate(root).code).toMatchSnapshot()
}) })
test('should NOT hoist elements with cached handlers', () => { test('should NOT cache elements with cached handlers', () => {
const root = transformWithHoist( const root = transformWithCache(
`<div><div><div @click="foo"/></div></div>`, `<div><div><div @click="foo"/></div></div>`,
{ {
prefixIdentifiers: true, prefixIdentifiers: true,
@ -549,7 +673,7 @@ describe('compiler: hoistStatic transform', () => {
}, },
) )
expect(root.cached).toBe(1) expect(root.cached.length).toBe(1)
expect(root.hoists.length).toBe(0) expect(root.hoists.length).toBe(0)
expect( expect(
generate(root, { generate(root, {
@ -559,8 +683,8 @@ describe('compiler: hoistStatic transform', () => {
).toMatchSnapshot() ).toMatchSnapshot()
}) })
test('should NOT hoist elements with cached handlers + other bindings', () => { test('should NOT cache elements with cached handlers + other bindings', () => {
const root = transformWithHoist( const root = transformWithCache(
`<div><div><div :class="{}" @click="foo"/></div></div>`, `<div><div><div :class="{}" @click="foo"/></div></div>`,
{ {
prefixIdentifiers: true, prefixIdentifiers: true,
@ -568,7 +692,7 @@ describe('compiler: hoistStatic transform', () => {
}, },
) )
expect(root.cached).toBe(1) expect(root.cached.length).toBe(1)
expect(root.hoists.length).toBe(0) expect(root.hoists.length).toBe(0)
expect( expect(
generate(root, { generate(root, {
@ -578,32 +702,66 @@ describe('compiler: hoistStatic transform', () => {
).toMatchSnapshot() ).toMatchSnapshot()
}) })
test('should NOT hoist keyed template v-for with plain element child', () => { test('should NOT cache keyed template v-for with plain element child', () => {
const root = transformWithHoist( const root = transformWithCache(
`<div><template v-for="item in items" :key="item"><span/></template></div>`, `<div><template v-for="item in items" :key="item"><span/></template></div>`,
) )
expect(root.hoists.length).toBe(0) expect(root.hoists.length).toBe(0)
expect(generate(root).code).toMatchSnapshot() expect(generate(root).code).toMatchSnapshot()
}) })
test('should NOT hoist SVG with directives', () => { test('should NOT cache SVG with directives', () => {
const root = transformWithHoist( const root = transformWithCache(
`<div><svg v-foo><path d="M2,3H5.5L12"/></svg></div>`, `<div><svg v-foo><path d="M2,3H5.5L12"/></svg></div>`,
) )
expect(root.hoists.length).toBe(2) expect(root.cached.length).toBe(1)
expect(root.codegenNode).toMatchObject({
children: [
{
tag: 'svg',
// only cache the children, not the svg tag itself
codegenNode: {
children: {
type: NodeTypes.JS_CACHE_EXPRESSION,
},
},
},
],
})
expect(generate(root).code).toMatchSnapshot() expect(generate(root).code).toMatchSnapshot()
}) })
test('clone hoisted array children in HMR mode', () => { test('clone hoisted array children in v-for + HMR mode', () => {
const root = transformWithHoist(`<div><span class="hi"></span></div>`, { const root = transformWithCache(
`<div><div v-for="i in 1"><span class="hi"></span></div></div>`,
{
hmr: true, hmr: true,
})
expect(root.hoists.length).toBe(2)
expect(root.codegenNode).toMatchObject({
children: {
content: '[..._hoisted_2]',
}, },
}) )
expect(root.cached.length).toBe(1)
const forBlockCodegen = (
(root.children[0] as ElementNode).children[0] as ForNode
).codegenNode
expect(forBlockCodegen).toMatchObject({
type: NodeTypes.VNODE_CALL,
tag: FRAGMENT,
props: undefined,
children: {
type: NodeTypes.JS_CALL_EXPRESSION,
callee: RENDER_LIST,
},
patchFlag: genFlagText(PatchFlags.UNKEYED_FRAGMENT),
})
const innerBlockCodegen = forBlockCodegen!.children.arguments[1]
expect(innerBlockCodegen.returns).toMatchObject({
type: NodeTypes.VNODE_CALL,
tag: `"div"`,
children: cachedChildrenArrayMatcher(
['span'],
true /* needArraySpread */,
),
})
expect(generate(root).code).toMatchSnapshot()
}) })
}) })
}) })

View File

@ -399,7 +399,7 @@ describe('compiler: transform v-model', () => {
prefixIdentifiers: true, prefixIdentifiers: true,
cacheHandlers: true, cacheHandlers: true,
}) })
expect(root.cached).toBe(1) expect(root.cached.length).toBe(1)
const codegen = (root.children[0] as PlainElementNode) const codegen = (root.children[0] as PlainElementNode)
.codegenNode as VNodeCall .codegenNode as VNodeCall
// should not list cached prop in dynamicProps // should not list cached prop in dynamicProps
@ -417,7 +417,7 @@ describe('compiler: transform v-model', () => {
cacheHandlers: true, cacheHandlers: true,
}, },
) )
expect(root.cached).toBe(0) expect(root.cached.length).toBe(0)
const codegen = ( const codegen = (
(root.children[0] as ForNode).children[0] as PlainElementNode (root.children[0] as ForNode).children[0] as PlainElementNode
).codegenNode as VNodeCall ).codegenNode as VNodeCall
@ -433,7 +433,7 @@ describe('compiler: transform v-model', () => {
cacheHandlers: true, cacheHandlers: true,
}) })
expect(root.cached).not.toBe(2) expect(root.cached).not.toBe(2)
expect(root.cached).toBe(1) expect(root.cached.length).toBe(1)
}) })
test('should mark update handler dynamic if it refers slot scope variables', () => { test('should mark update handler dynamic if it refers slot scope variables', () => {

View File

@ -504,7 +504,7 @@ describe('compiler: transform v-on', () => {
prefixIdentifiers: true, prefixIdentifiers: true,
cacheHandlers: true, cacheHandlers: true,
}) })
expect(root.cached).toBe(1) expect(root.cached.length).toBe(1)
const vnodeCall = node.codegenNode as VNodeCall const vnodeCall = node.codegenNode as VNodeCall
// should not treat cached handler as dynamicProp, so no flags // should not treat cached handler as dynamicProp, so no flags
expect(vnodeCall.patchFlag).toBeUndefined() expect(vnodeCall.patchFlag).toBeUndefined()
@ -525,7 +525,7 @@ describe('compiler: transform v-on', () => {
prefixIdentifiers: true, prefixIdentifiers: true,
cacheHandlers: true, cacheHandlers: true,
}) })
expect(root.cached).toBe(1) expect(root.cached.length).toBe(1)
const vnodeCall = node.codegenNode as VNodeCall const vnodeCall = node.codegenNode as VNodeCall
// should not treat cached handler as dynamicProp, so no flags // should not treat cached handler as dynamicProp, so no flags
expect(vnodeCall.patchFlag).toBeUndefined() expect(vnodeCall.patchFlag).toBeUndefined()
@ -550,7 +550,7 @@ describe('compiler: transform v-on', () => {
prefixIdentifiers: true, prefixIdentifiers: true,
cacheHandlers: true, cacheHandlers: true,
}) })
expect(root.cached).toBe(1) expect(root.cached.length).toBe(1)
const vnodeCall = node.codegenNode as VNodeCall const vnodeCall = node.codegenNode as VNodeCall
// should not treat cached handler as dynamicProp, so no flags // should not treat cached handler as dynamicProp, so no flags
expect(vnodeCall.patchFlag).toBeUndefined() expect(vnodeCall.patchFlag).toBeUndefined()
@ -587,7 +587,7 @@ describe('compiler: transform v-on', () => {
cacheHandlers: true, cacheHandlers: true,
isNativeTag: tag => tag === 'div', isNativeTag: tag => tag === 'div',
}) })
expect(root.cached).toBe(0) expect(root.cached.length).toBe(0)
}) })
test('should not be cached inside v-once', () => { test('should not be cached inside v-once', () => {
@ -599,7 +599,7 @@ describe('compiler: transform v-on', () => {
}, },
) )
expect(root.cached).not.toBe(2) expect(root.cached).not.toBe(2)
expect(root.cached).toBe(1) expect(root.cached.length).toBe(1)
}) })
test('inline function expression handler', () => { test('inline function expression handler', () => {
@ -607,7 +607,7 @@ describe('compiler: transform v-on', () => {
prefixIdentifiers: true, prefixIdentifiers: true,
cacheHandlers: true, cacheHandlers: true,
}) })
expect(root.cached).toBe(1) expect(root.cached.length).toBe(1)
const vnodeCall = node.codegenNode as VNodeCall const vnodeCall = node.codegenNode as VNodeCall
// should not treat cached handler as dynamicProp, so no flags // should not treat cached handler as dynamicProp, so no flags
expect(vnodeCall.patchFlag).toBeUndefined() expect(vnodeCall.patchFlag).toBeUndefined()
@ -631,7 +631,7 @@ describe('compiler: transform v-on', () => {
cacheHandlers: true, cacheHandlers: true,
}, },
) )
expect(root.cached).toBe(1) expect(root.cached.length).toBe(1)
const vnodeCall = node.codegenNode as VNodeCall const vnodeCall = node.codegenNode as VNodeCall
// should not treat cached handler as dynamicProp, so no flags // should not treat cached handler as dynamicProp, so no flags
expect(vnodeCall.patchFlag).toBeUndefined() expect(vnodeCall.patchFlag).toBeUndefined()
@ -656,7 +656,7 @@ describe('compiler: transform v-on', () => {
}, },
) )
expect(root.cached).toBe(1) expect(root.cached.length).toBe(1)
const vnodeCall = node.codegenNode as VNodeCall const vnodeCall = node.codegenNode as VNodeCall
// should not treat cached handler as dynamicProp, so no flags // should not treat cached handler as dynamicProp, so no flags
expect(vnodeCall.patchFlag).toBeUndefined() expect(vnodeCall.patchFlag).toBeUndefined()
@ -688,7 +688,7 @@ describe('compiler: transform v-on', () => {
cacheHandlers: true, cacheHandlers: true,
}, },
) )
expect(root.cached).toBe(1) expect(root.cached.length).toBe(1)
const vnodeCall = node.codegenNode as VNodeCall const vnodeCall = node.codegenNode as VNodeCall
// should not treat cached handler as dynamicProp, so no flags // should not treat cached handler as dynamicProp, so no flags
expect(vnodeCall.patchFlag).toBeUndefined() expect(vnodeCall.patchFlag).toBeUndefined()
@ -713,8 +713,8 @@ describe('compiler: transform v-on', () => {
prefixIdentifiers: true, prefixIdentifiers: true,
cacheHandlers: true, cacheHandlers: true,
}) })
expect(root.cached).toBe(1) expect(root.cached.length).toBe(1)
expect(root.cached).toBe(1) expect(root.cached.length).toBe(1)
const vnodeCall = node.codegenNode as VNodeCall const vnodeCall = node.codegenNode as VNodeCall
// should not treat cached handler as dynamicProp, so no flags // should not treat cached handler as dynamicProp, so no flags
expect(vnodeCall.patchFlag).toBeUndefined() expect(vnodeCall.patchFlag).toBeUndefined()

View File

@ -22,7 +22,7 @@ function transformWithOnce(template: string, options: CompilerOptions = {}) {
describe('compiler: v-once transform', () => { describe('compiler: v-once transform', () => {
test('as root node', () => { test('as root node', () => {
const root = transformWithOnce(`<div :id="foo" v-once />`) const root = transformWithOnce(`<div :id="foo" v-once />`)
expect(root.cached).toBe(1) expect(root.cached.length).toBe(1)
expect(root.helpers).toContain(SET_BLOCK_TRACKING) expect(root.helpers).toContain(SET_BLOCK_TRACKING)
expect(root.codegenNode).toMatchObject({ expect(root.codegenNode).toMatchObject({
type: NodeTypes.JS_CACHE_EXPRESSION, type: NodeTypes.JS_CACHE_EXPRESSION,
@ -37,7 +37,7 @@ describe('compiler: v-once transform', () => {
test('on nested plain element', () => { test('on nested plain element', () => {
const root = transformWithOnce(`<div><div :id="foo" v-once /></div>`) const root = transformWithOnce(`<div><div :id="foo" v-once /></div>`)
expect(root.cached).toBe(1) expect(root.cached.length).toBe(1)
expect(root.helpers).toContain(SET_BLOCK_TRACKING) expect(root.helpers).toContain(SET_BLOCK_TRACKING)
expect((root.children[0] as any).children[0].codegenNode).toMatchObject({ expect((root.children[0] as any).children[0].codegenNode).toMatchObject({
type: NodeTypes.JS_CACHE_EXPRESSION, type: NodeTypes.JS_CACHE_EXPRESSION,
@ -52,7 +52,7 @@ describe('compiler: v-once transform', () => {
test('on component', () => { test('on component', () => {
const root = transformWithOnce(`<div><Comp :id="foo" v-once /></div>`) const root = transformWithOnce(`<div><Comp :id="foo" v-once /></div>`)
expect(root.cached).toBe(1) expect(root.cached.length).toBe(1)
expect(root.helpers).toContain(SET_BLOCK_TRACKING) expect(root.helpers).toContain(SET_BLOCK_TRACKING)
expect((root.children[0] as any).children[0].codegenNode).toMatchObject({ expect((root.children[0] as any).children[0].codegenNode).toMatchObject({
type: NodeTypes.JS_CACHE_EXPRESSION, type: NodeTypes.JS_CACHE_EXPRESSION,
@ -67,7 +67,7 @@ describe('compiler: v-once transform', () => {
test('on slot outlet', () => { test('on slot outlet', () => {
const root = transformWithOnce(`<div><slot v-once /></div>`) const root = transformWithOnce(`<div><slot v-once /></div>`)
expect(root.cached).toBe(1) expect(root.cached.length).toBe(1)
expect(root.helpers).toContain(SET_BLOCK_TRACKING) expect(root.helpers).toContain(SET_BLOCK_TRACKING)
expect((root.children[0] as any).children[0].codegenNode).toMatchObject({ expect((root.children[0] as any).children[0].codegenNode).toMatchObject({
type: NodeTypes.JS_CACHE_EXPRESSION, type: NodeTypes.JS_CACHE_EXPRESSION,
@ -84,7 +84,7 @@ describe('compiler: v-once transform', () => {
test('inside v-once', () => { test('inside v-once', () => {
const root = transformWithOnce(`<div v-once><div v-once/></div>`) const root = transformWithOnce(`<div v-once><div v-once/></div>`)
expect(root.cached).not.toBe(2) expect(root.cached).not.toBe(2)
expect(root.cached).toBe(1) expect(root.cached.length).toBe(1)
}) })
// cached nodes should be ignored by hoistStatic transform // cached nodes should be ignored by hoistStatic transform
@ -92,7 +92,7 @@ describe('compiler: v-once transform', () => {
const root = transformWithOnce(`<div><div v-once /></div>`, { const root = transformWithOnce(`<div><div v-once /></div>`, {
hoistStatic: true, hoistStatic: true,
}) })
expect(root.cached).toBe(1) expect(root.cached.length).toBe(1)
expect(root.helpers).toContain(SET_BLOCK_TRACKING) expect(root.helpers).toContain(SET_BLOCK_TRACKING)
expect(root.hoists.length).toBe(0) expect(root.hoists.length).toBe(0)
expect((root.children[0] as any).children[0].codegenNode).toMatchObject({ expect((root.children[0] as any).children[0].codegenNode).toMatchObject({
@ -108,7 +108,7 @@ describe('compiler: v-once transform', () => {
test('with v-if/else', () => { test('with v-if/else', () => {
const root = transformWithOnce(`<div v-if="BOOLEAN" v-once /><p v-else/>`) const root = transformWithOnce(`<div v-if="BOOLEAN" v-once /><p v-else/>`)
expect(root.cached).toBe(1) expect(root.cached.length).toBe(1)
expect(root.helpers).toContain(SET_BLOCK_TRACKING) expect(root.helpers).toContain(SET_BLOCK_TRACKING)
expect(root.children[0]).toMatchObject({ expect(root.children[0]).toMatchObject({
type: NodeTypes.IF, type: NodeTypes.IF,
@ -132,7 +132,7 @@ describe('compiler: v-once transform', () => {
test('with v-for', () => { test('with v-for', () => {
const root = transformWithOnce(`<div v-for="i in list" v-once />`) const root = transformWithOnce(`<div v-for="i in list" v-once />`)
expect(root.cached).toBe(1) expect(root.cached.length).toBe(1)
expect(root.helpers).toContain(SET_BLOCK_TRACKING) expect(root.helpers).toContain(SET_BLOCK_TRACKING)
expect(root.children[0]).toMatchObject({ expect(root.children[0]).toMatchObject({
type: NodeTypes.FOR, type: NodeTypes.FOR,

View File

@ -110,7 +110,7 @@ export interface RootNode extends Node {
directives: string[] directives: string[]
hoists: (JSChildNode | null)[] hoists: (JSChildNode | null)[]
imports: ImportItem[] imports: ImportItem[]
cached: number cached: (CacheExpression | null)[]
temps: number temps: number
ssrHelpers?: symbol[] ssrHelpers?: symbol[]
codegenNode?: TemplateChildNode | JSChildNode | BlockStatement codegenNode?: TemplateChildNode | JSChildNode | BlockStatement
@ -218,7 +218,7 @@ export interface DirectiveNode extends Node {
export enum ConstantTypes { export enum ConstantTypes {
NOT_CONSTANT = 0, NOT_CONSTANT = 0,
CAN_SKIP_PATCH, CAN_SKIP_PATCH,
CAN_HOIST, CAN_CACHE,
CAN_STRINGIFY, CAN_STRINGIFY,
} }
@ -330,6 +330,7 @@ export interface VNodeCall extends Node {
| SlotsExpression // component slots | SlotsExpression // component slots
| ForRenderListExpression // v-for fragment call | ForRenderListExpression // v-for fragment call
| SimpleExpressionNode // hoisted | SimpleExpressionNode // hoisted
| CacheExpression // cached
| undefined | undefined
patchFlag: string | undefined patchFlag: string | undefined
dynamicProps: string | SimpleExpressionNode | undefined dynamicProps: string | SimpleExpressionNode | undefined
@ -416,7 +417,8 @@ export interface CacheExpression extends Node {
type: NodeTypes.JS_CACHE_EXPRESSION type: NodeTypes.JS_CACHE_EXPRESSION
index: number index: number
value: JSChildNode value: JSChildNode
isVNode: boolean needPauseTracking: boolean
needArraySpread: boolean
} }
export interface MemoExpression extends CallExpression { export interface MemoExpression extends CallExpression {
@ -511,7 +513,7 @@ export interface SlotsObjectProperty extends Property {
} }
export interface SlotFunctionExpression extends FunctionExpression { export interface SlotFunctionExpression extends FunctionExpression {
returns: TemplateChildNode[] returns: TemplateChildNode[] | CacheExpression
} }
// createSlots({ ... }, [ // createSlots({ ... }, [
@ -598,7 +600,7 @@ export function createRoot(
directives: [], directives: [],
hoists: [], hoists: [],
imports: [], imports: [],
cached: 0, cached: [],
temps: 0, temps: 0,
codegenNode: undefined, codegenNode: undefined,
loc: locStub, loc: locStub,
@ -771,13 +773,14 @@ export function createConditionalExpression(
export function createCacheExpression( export function createCacheExpression(
index: number, index: number,
value: JSChildNode, value: JSChildNode,
isVNode: boolean = false, needPauseTracking: boolean = false,
): CacheExpression { ): CacheExpression {
return { return {
type: NodeTypes.JS_CACHE_EXPRESSION, type: NodeTypes.JS_CACHE_EXPRESSION,
index, index,
value, value,
isVNode, needPauseTracking: needPauseTracking,
needArraySpread: false,
loc: locStub, loc: locStub,
} }
} }

View File

@ -43,8 +43,6 @@ import {
CREATE_TEXT, CREATE_TEXT,
CREATE_VNODE, CREATE_VNODE,
OPEN_BLOCK, OPEN_BLOCK,
POP_SCOPE_ID,
PUSH_SCOPE_ID,
RESOLVE_COMPONENT, RESOLVE_COMPONENT,
RESOLVE_DIRECTIVE, RESOLVE_DIRECTIVE,
RESOLVE_FILTER, RESOLVE_FILTER,
@ -473,11 +471,6 @@ function genModulePreamble(
ssrRuntimeModuleName, ssrRuntimeModuleName,
} = context } = context
if (genScopeId && ast.hoists.length) {
ast.helpers.add(PUSH_SCOPE_ID)
ast.helpers.add(POP_SCOPE_ID)
}
// generate import statements for helpers // generate import statements for helpers
if (ast.helpers.size) { if (ast.helpers.size) {
const helpers = Array.from(ast.helpers) const helpers = Array.from(ast.helpers)
@ -566,33 +559,14 @@ function genHoists(hoists: (JSChildNode | null)[], context: CodegenContext) {
return return
} }
context.pure = true context.pure = true
const { push, newline, helper, scopeId, mode } = context const { push, newline } = context
const genScopeId = !__BROWSER__ && scopeId != null && mode !== 'function'
newline() newline()
// generate inlined withScopeId helper
if (genScopeId) {
push(
`const _withScopeId = n => (${helper(
PUSH_SCOPE_ID,
)}("${scopeId}"),n=n(),${helper(POP_SCOPE_ID)}(),n)`,
)
newline()
}
for (let i = 0; i < hoists.length; i++) { for (let i = 0; i < hoists.length; i++) {
const exp = hoists[i] const exp = hoists[i]
if (exp) { if (exp) {
const needScopeIdWrapper = genScopeId && exp.type === NodeTypes.VNODE_CALL push(`const _hoisted_${i + 1} = `)
push(
`const _hoisted_${i + 1} = ${
needScopeIdWrapper ? `${PURE_ANNOTATION} _withScopeId(() => ` : ``
}`,
)
genNode(exp, context) genNode(exp, context)
if (needScopeIdWrapper) {
push(`)`)
}
newline() newline()
} }
} }
@ -1007,15 +981,19 @@ function genConditionalExpression(
function genCacheExpression(node: CacheExpression, context: CodegenContext) { function genCacheExpression(node: CacheExpression, context: CodegenContext) {
const { push, helper, indent, deindent, newline } = context const { push, helper, indent, deindent, newline } = context
const { needPauseTracking, needArraySpread } = node
if (needArraySpread) {
push(`[...(`)
}
push(`_cache[${node.index}] || (`) push(`_cache[${node.index}] || (`)
if (node.isVNode) { if (needPauseTracking) {
indent() indent()
push(`${helper(SET_BLOCK_TRACKING)}(-1),`) push(`${helper(SET_BLOCK_TRACKING)}(-1),`)
newline() newline()
} }
push(`_cache[${node.index}] = `) push(`_cache[${node.index}] = `)
genNode(node.value, context) genNode(node.value, context)
if (node.isVNode) { if (needPauseTracking) {
push(`,`) push(`,`)
newline() newline()
push(`${helper(SET_BLOCK_TRACKING)}(1),`) push(`${helper(SET_BLOCK_TRACKING)}(1),`)
@ -1024,6 +1002,9 @@ function genCacheExpression(node: CacheExpression, context: CodegenContext) {
deindent() deindent()
} }
push(`)`) push(`)`)
if (needArraySpread) {
push(`)]`)
}
} }
function genTemplateLiteral(node: TemplateLiteral, context: CodegenContext) { function genTemplateLiteral(node: TemplateLiteral, context: CodegenContext) {

View File

@ -67,7 +67,7 @@ export {
type PropsExpression, type PropsExpression,
} from './transforms/transformElement' } from './transforms/transformElement'
export { processSlotOutlet } from './transforms/transformSlotOutlet' export { processSlotOutlet } from './transforms/transformSlotOutlet'
export { getConstantType } from './transforms/hoistStatic' export { getConstantType } from './transforms/cacheStatic'
export { generateCodeFrame } from '@vue/shared' export { generateCodeFrame } from '@vue/shared'
// v2 compat only // v2 compat only

View File

@ -250,7 +250,7 @@ export interface TransformOptions
*/ */
prefixIdentifiers?: boolean prefixIdentifiers?: boolean
/** /**
* Hoist static VNodes and props objects to `_hoisted_x` constants * Cache static VNodes and props objects to `_hoisted_x` constants
* @default false * @default false
*/ */
hoistStatic?: boolean hoistStatic?: boolean

View File

@ -32,7 +32,14 @@ export const CAMELIZE = Symbol(__DEV__ ? `camelize` : ``)
export const CAPITALIZE = Symbol(__DEV__ ? `capitalize` : ``) export const CAPITALIZE = Symbol(__DEV__ ? `capitalize` : ``)
export const TO_HANDLER_KEY = Symbol(__DEV__ ? `toHandlerKey` : ``) export const TO_HANDLER_KEY = Symbol(__DEV__ ? `toHandlerKey` : ``)
export const SET_BLOCK_TRACKING = Symbol(__DEV__ ? `setBlockTracking` : ``) export const SET_BLOCK_TRACKING = Symbol(__DEV__ ? `setBlockTracking` : ``)
/**
* @deprecated no longer needed in 3.5+ because we no longer hoist element nodes
* but kept for backwards compat
*/
export const PUSH_SCOPE_ID = Symbol(__DEV__ ? `pushScopeId` : ``) export const PUSH_SCOPE_ID = Symbol(__DEV__ ? `pushScopeId` : ``)
/**
* @deprecated kept for backwards compat
*/
export const POP_SCOPE_ID = Symbol(__DEV__ ? `popScopeId` : ``) export const POP_SCOPE_ID = Symbol(__DEV__ ? `popScopeId` : ``)
export const WITH_CTX = Symbol(__DEV__ ? `withCtx` : ``) export const WITH_CTX = Symbol(__DEV__ ? `withCtx` : ``)
export const UNREF = Symbol(__DEV__ ? `unref` : ``) export const UNREF = Symbol(__DEV__ ? `unref` : ``)

View File

@ -38,7 +38,7 @@ import {
helperNameMap, helperNameMap,
} from './runtimeHelpers' } from './runtimeHelpers'
import { isVSlot } from './utils' import { isVSlot } from './utils'
import { hoistStatic, isSingleElementRoot } from './transforms/hoistStatic' import { cacheStatic, isSingleElementRoot } from './transforms/cacheStatic'
import type { CompilerCompatOptions } from './compat/compatConfig' import type { CompilerCompatOptions } from './compat/compatConfig'
// There are two types of transforms: // There are two types of transforms:
@ -93,7 +93,7 @@ export interface TransformContext
hoists: (JSChildNode | null)[] hoists: (JSChildNode | null)[]
imports: ImportItem[] imports: ImportItem[]
temps: number temps: number
cached: number cached: (CacheExpression | null)[]
identifiers: { [name: string]: number | undefined } identifiers: { [name: string]: number | undefined }
scopes: { scopes: {
vFor: number vFor: number
@ -117,7 +117,7 @@ export interface TransformContext
addIdentifiers(exp: ExpressionNode | string): void addIdentifiers(exp: ExpressionNode | string): void
removeIdentifiers(exp: ExpressionNode | string): void removeIdentifiers(exp: ExpressionNode | string): void
hoist(exp: string | JSChildNode | ArrayExpression): SimpleExpressionNode hoist(exp: string | JSChildNode | ArrayExpression): SimpleExpressionNode
cache<T extends JSChildNode>(exp: T, isVNode?: boolean): CacheExpression | T cache(exp: JSChildNode, isVNode?: boolean): CacheExpression
constantCache: WeakMap<TemplateChildNode, ConstantTypes> constantCache: WeakMap<TemplateChildNode, ConstantTypes>
// 2.x Compat only // 2.x Compat only
@ -185,9 +185,9 @@ export function createTransformContext(
directives: new Set(), directives: new Set(),
hoists: [], hoists: [],
imports: [], imports: [],
cached: [],
constantCache: new WeakMap(), constantCache: new WeakMap(),
temps: 0, temps: 0,
cached: 0,
identifiers: Object.create(null), identifiers: Object.create(null),
scopes: { scopes: {
vFor: 0, vFor: 0,
@ -291,13 +291,19 @@ export function createTransformContext(
`_hoisted_${context.hoists.length}`, `_hoisted_${context.hoists.length}`,
false, false,
exp.loc, exp.loc,
ConstantTypes.CAN_HOIST, ConstantTypes.CAN_CACHE,
) )
identifier.hoisted = exp identifier.hoisted = exp
return identifier return identifier
}, },
cache(exp, isVNode = false) { cache(exp, isVNode = false) {
return createCacheExpression(context.cached++, exp, isVNode) const cacheExp = createCacheExpression(
context.cached.length,
exp,
isVNode,
)
context.cached.push(cacheExp)
return cacheExp
}, },
} }
@ -324,7 +330,7 @@ export function transform(root: RootNode, options: TransformOptions) {
const context = createTransformContext(root, options) const context = createTransformContext(root, options)
traverseNode(root, context) traverseNode(root, context)
if (options.hoistStatic) { if (options.hoistStatic) {
hoistStatic(root, context) cacheStatic(root, context)
} }
if (!options.ssr) { if (!options.ssr) {
createRootCodegen(root, context) createRootCodegen(root, context)

View File

@ -1,16 +1,20 @@
import { import {
type CacheExpression,
type CallExpression, type CallExpression,
type ComponentNode, type ComponentNode,
ConstantTypes, ConstantTypes,
ElementTypes, ElementTypes,
type ExpressionNode,
type JSChildNode, type JSChildNode,
NodeTypes, NodeTypes,
type ParentNode, type ParentNode,
type PlainElementNode, type PlainElementNode,
type RootNode, type RootNode,
type SimpleExpressionNode, type SimpleExpressionNode,
type SlotFunctionExpression,
type TemplateChildNode, type TemplateChildNode,
type TemplateNode, type TemplateNode,
type TextCallNode,
type VNodeCall, type VNodeCall,
createArrayExpression, createArrayExpression,
getVNodeBlockHelper, getVNodeBlockHelper,
@ -18,7 +22,7 @@ import {
} from '../ast' } from '../ast'
import type { TransformContext } from '../transform' import type { TransformContext } from '../transform'
import { PatchFlags, isArray, isString, isSymbol } from '@vue/shared' import { PatchFlags, isArray, isString, isSymbol } from '@vue/shared'
import { isSlotOutlet } from '../utils' import { findDir, isSlotOutlet } from '../utils'
import { import {
GUARD_REACTIVE_PROPS, GUARD_REACTIVE_PROPS,
NORMALIZE_CLASS, NORMALIZE_CLASS,
@ -27,9 +31,10 @@ import {
OPEN_BLOCK, OPEN_BLOCK,
} from '../runtimeHelpers' } from '../runtimeHelpers'
export function hoistStatic(root: RootNode, context: TransformContext) { export function cacheStatic(root: RootNode, context: TransformContext) {
walk( walk(
root, root,
undefined,
context, context,
// Root node is unfortunately non-hoistable due to potential parent // Root node is unfortunately non-hoistable due to potential parent
// fallthrough attributes. // fallthrough attributes.
@ -51,16 +56,16 @@ export function isSingleElementRoot(
function walk( function walk(
node: ParentNode, node: ParentNode,
parent: ParentNode | undefined,
context: TransformContext, context: TransformContext,
doNotHoistNode: boolean = false, doNotHoistNode: boolean = false,
inFor = false,
) { ) {
const { children } = node const { children } = node
const originalCount = children.length const toCache: (PlainElementNode | TextCallNode)[] = []
let hoistedCount = 0
for (let i = 0; i < children.length; i++) { for (let i = 0; i < children.length; i++) {
const child = children[i] const child = children[i]
// only plain elements & text calls are eligible for hoisting. // only plain elements & text calls are eligible for caching.
if ( if (
child.type === NodeTypes.ELEMENT && child.type === NodeTypes.ELEMENT &&
child.tagType === ElementTypes.ELEMENT child.tagType === ElementTypes.ELEMENT
@ -69,11 +74,10 @@ function walk(
? ConstantTypes.NOT_CONSTANT ? ConstantTypes.NOT_CONSTANT
: getConstantType(child, context) : getConstantType(child, context)
if (constantType > ConstantTypes.NOT_CONSTANT) { if (constantType > ConstantTypes.NOT_CONSTANT) {
if (constantType >= ConstantTypes.CAN_HOIST) { if (constantType >= ConstantTypes.CAN_CACHE) {
;(child.codegenNode as VNodeCall).patchFlag = ;(child.codegenNode as VNodeCall).patchFlag =
PatchFlags.HOISTED + (__DEV__ ? ` /* HOISTED */` : ``) PatchFlags.CACHED + (__DEV__ ? ` /* CACHED */` : ``)
child.codegenNode = context.hoist(child.codegenNode!) toCache.push(child)
hoistedCount++
continue continue
} }
} else { } else {
@ -87,7 +91,7 @@ function walk(
flag === PatchFlags.NEED_PATCH || flag === PatchFlags.NEED_PATCH ||
flag === PatchFlags.TEXT) && flag === PatchFlags.TEXT) &&
getGeneratedPropsConstantType(child, context) >= getGeneratedPropsConstantType(child, context) >=
ConstantTypes.CAN_HOIST ConstantTypes.CAN_CACHE
) { ) {
const props = getNodeProps(child) const props = getNodeProps(child)
if (props) { if (props) {
@ -99,6 +103,14 @@ function walk(
} }
} }
} }
} else if (child.type === NodeTypes.TEXT_CALL) {
const constantType = doNotHoistNode
? ConstantTypes.NOT_CONSTANT
: getConstantType(child, context)
if (constantType >= ConstantTypes.CAN_CACHE) {
toCache.push(child)
continue
}
} }
// walk further // walk further
@ -107,54 +119,122 @@ function walk(
if (isComponent) { if (isComponent) {
context.scopes.vSlot++ context.scopes.vSlot++
} }
walk(child, context) walk(child, node, context, false, inFor)
if (isComponent) { if (isComponent) {
context.scopes.vSlot-- context.scopes.vSlot--
} }
} else if (child.type === NodeTypes.FOR) { } else if (child.type === NodeTypes.FOR) {
// Do not hoist v-for single child because it has to be a block // Do not hoist v-for single child because it has to be a block
walk(child, context, child.children.length === 1) walk(child, node, context, child.children.length === 1, true)
} 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++) {
// Do not hoist v-if single child because it has to be a block // Do not hoist v-if single child because it has to be a block
walk( walk(
child.branches[i], child.branches[i],
node,
context, context,
child.branches[i].children.length === 1, child.branches[i].children.length === 1,
inFor,
) )
} }
} }
} }
if (hoistedCount && context.transformHoist) { let cachedAsArray = false
context.transformHoist(children, context, node) if (toCache.length === children.length && node.type === NodeTypes.ELEMENT) {
}
// all children were hoisted - the entire children array is hoistable.
if ( if (
hoistedCount &&
hoistedCount === originalCount &&
node.type === NodeTypes.ELEMENT &&
node.tagType === ElementTypes.ELEMENT && node.tagType === ElementTypes.ELEMENT &&
node.codegenNode && node.codegenNode &&
node.codegenNode.type === NodeTypes.VNODE_CALL && node.codegenNode.type === NodeTypes.VNODE_CALL &&
isArray(node.codegenNode.children) isArray(node.codegenNode.children)
) { ) {
const hoisted = context.hoist( // all children were hoisted - the entire children array is cacheable.
node.codegenNode.children = getCacheExpression(
createArrayExpression(node.codegenNode.children), createArrayExpression(node.codegenNode.children),
) )
// #6978, #7138, #7114 cachedAsArray = true
// a hoisted children array inside v-for can caused HMR errors since } else if (
// it might be mutated when mounting the v-for list node.tagType === ElementTypes.COMPONENT &&
if (context.hmr) { node.codegenNode &&
hoisted.content = `[...${hoisted.content}]` node.codegenNode.type === NodeTypes.VNODE_CALL &&
node.codegenNode.children &&
!isArray(node.codegenNode.children) &&
node.codegenNode.children.type === NodeTypes.JS_OBJECT_EXPRESSION
) {
// default slot
const slot = getSlotNode(node.codegenNode, 'default')
if (slot) {
slot.returns = getCacheExpression(
createArrayExpression(slot.returns as TemplateChildNode[]),
)
cachedAsArray = true
} }
node.codegenNode.children = hoisted } else if (
node.tagType === ElementTypes.TEMPLATE &&
parent &&
parent.type === NodeTypes.ELEMENT &&
parent.tagType === ElementTypes.COMPONENT &&
parent.codegenNode &&
parent.codegenNode.type === NodeTypes.VNODE_CALL &&
parent.codegenNode.children &&
!isArray(parent.codegenNode.children) &&
parent.codegenNode.children.type === NodeTypes.JS_OBJECT_EXPRESSION
) {
// named <template> slot
const slotName = findDir(node, 'slot', true)
const slot =
slotName &&
slotName.arg &&
getSlotNode(parent.codegenNode, slotName.arg)
if (slot) {
slot.returns = getCacheExpression(
createArrayExpression(slot.returns as TemplateChildNode[]),
)
cachedAsArray = true
}
}
}
if (!cachedAsArray) {
for (const child of toCache) {
child.codegenNode = context.cache(child.codegenNode!)
}
}
function getCacheExpression(value: JSChildNode): CacheExpression {
const exp = context.cache(value)
// #6978, #7138, #7114
// a cached children array inside v-for can caused HMR errors since
// it might be mutated when mounting the first item
if (inFor && context.hmr) {
exp.needArraySpread = true
}
return exp
}
function getSlotNode(
node: VNodeCall,
name: string | ExpressionNode,
): SlotFunctionExpression | undefined {
if (
node.children &&
!isArray(node.children) &&
node.children.type === NodeTypes.JS_OBJECT_EXPRESSION
) {
const slot = node.children.properties.find(
p => p.key === name || (p.key as SimpleExpressionNode).content === name,
)
return slot && slot.value
}
}
if (toCache.length && context.transformHoist) {
context.transformHoist(children, context, node)
} }
} }
export function getConstantType( export function getConstantType(
node: TemplateChildNode | SimpleExpressionNode, node: TemplateChildNode | SimpleExpressionNode | CacheExpression,
context: TransformContext, context: TransformContext,
): ConstantTypes { ): ConstantTypes {
const { constantCache } = context const { constantCache } = context
@ -284,6 +364,8 @@ export function getConstantType(
} }
} }
return returnType return returnType
case NodeTypes.JS_CACHE_EXPRESSION:
return ConstantTypes.CAN_CACHE
default: default:
if (__DEV__) { if (__DEV__) {
const exhaustiveCheck: never = node const exhaustiveCheck: never = node

View File

@ -57,7 +57,7 @@ import {
toValidAssetId, toValidAssetId,
} from '../utils' } from '../utils'
import { buildSlots } from './vSlot' import { buildSlots } from './vSlot'
import { getConstantType } from './hoistStatic' import { getConstantType } from './cacheStatic'
import { BindingTypes } from '../options' import { BindingTypes } from '../options'
import { import {
CompilerDeprecationTypes, CompilerDeprecationTypes,

View File

@ -250,7 +250,7 @@ export function processExpression(
if (isLiteral) { if (isLiteral) {
node.constType = ConstantTypes.CAN_STRINGIFY node.constType = ConstantTypes.CAN_STRINGIFY
} else { } else {
node.constType = ConstantTypes.CAN_HOIST node.constType = ConstantTypes.CAN_CACHE
} }
} }
return node return node

View File

@ -11,7 +11,7 @@ import {
import { isText } from '../utils' import { isText } from '../utils'
import { CREATE_TEXT } from '../runtimeHelpers' import { CREATE_TEXT } from '../runtimeHelpers'
import { PatchFlagNames, PatchFlags } from '@vue/shared' import { PatchFlagNames, PatchFlags } from '@vue/shared'
import { getConstantType } from './hoistStatic' import { getConstantType } from './cacheStatic'
// Merge adjacent text nodes and expressions into a single expression // Merge adjacent text nodes and expressions into a single expression
// e.g. <div>abc {{ d }} {{ e }}</div> should have a single expression node as child. // e.g. <div>abc {{ d }} {{ e }}</div> should have a single expression node as child.

View File

@ -224,8 +224,10 @@ export const transformFor = createStructuralDirectiveTransform(
renderExp.arguments.push( renderExp.arguments.push(
loop as ForIteratorExpression, loop as ForIteratorExpression,
createSimpleExpression(`_cache`), createSimpleExpression(`_cache`),
createSimpleExpression(String(context.cached++)), createSimpleExpression(String(context.cached.length)),
) )
// increment cache count
context.cached.push(null)
} else { } else {
renderExp.arguments.push( renderExp.arguments.push(
createFunctionExpression( createFunctionExpression(

View File

@ -248,7 +248,7 @@ function createChildrenCodegenNode(
`${keyIndex}`, `${keyIndex}`,
false, false,
locStub, locStub,
ConstantTypes.CAN_HOIST, ConstantTypes.CAN_CACHE,
), ),
) )
const { children } = branch const { children } = branch

View File

@ -33,8 +33,10 @@ export const transformMemo: NodeTransform = (node, context) => {
dir.exp!, dir.exp!,
createFunctionExpression(undefined, codegenNode), createFunctionExpression(undefined, codegenNode),
`_cache`, `_cache`,
String(context.cached++), String(context.cached.length),
]) as MemoExpression ]) as MemoExpression
// increment cache count
context.cached.push(null)
} }
} }
} }

View File

@ -148,7 +148,7 @@ export const transformModel: DirectiveTransform = (dir, node, context) => {
`{ ${modifiers} }`, `{ ${modifiers} }`,
false, false,
dir.loc, dir.loc,
ConstantTypes.CAN_HOIST, ConstantTypes.CAN_CACHE,
), ),
), ),
) )

View File

@ -1,5 +1,6 @@
import { import {
type BlockCodegenNode, type BlockCodegenNode,
type CacheExpression,
type CallExpression, type CallExpression,
type DirectiveNode, type DirectiveNode,
type ElementNode, type ElementNode,
@ -438,7 +439,12 @@ export function toValidAssetId(
// Check if a node contains expressions that reference current context scope ids // Check if a node contains expressions that reference current context scope ids
export function hasScopeRef( export function hasScopeRef(
node: TemplateChildNode | IfBranchNode | ExpressionNode | undefined, node:
| TemplateChildNode
| IfBranchNode
| ExpressionNode
| CacheExpression
| undefined,
ids: TransformContext['identifiers'], ids: TransformContext['identifiers'],
): boolean { ): boolean {
if (!node || Object.keys(ids).length === 0) { if (!node || Object.keys(ids).length === 0) {
@ -481,6 +487,7 @@ export function hasScopeRef(
return hasScopeRef(node.content, ids) return hasScopeRef(node.content, ids)
case NodeTypes.TEXT: case NodeTypes.TEXT:
case NodeTypes.COMMENT: case NodeTypes.COMMENT:
case NodeTypes.JS_CACHE_EXPRESSION:
return false return false
default: default:
if (__DEV__) { if (__DEV__) {

View File

@ -1,96 +1,128 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`stringify static html > escape 1`] = `
"const { toDisplayString: _toDisplayString, normalizeClass: _normalizeClass, createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
_createStaticVNode("<div><span class=\\"foo&gt;ar\\">1 + &lt;</span><span>&amp;</span><span class=\\"foo&gt;ar\\">1 + &lt;</span><span>&amp;</span><span class=\\"foo&gt;ar\\">1 + &lt;</span><span>&amp;</span><span class=\\"foo&gt;ar\\">1 + &lt;</span><span>&amp;</span><span class=\\"foo&gt;ar\\">1 + &lt;</span><span>&amp;</span></div>", 1)
])))
}"
`;
exports[`stringify static html > serializing constant bindings 1`] = `
"const { toDisplayString: _toDisplayString, normalizeClass: _normalizeClass, createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
_createStaticVNode("<div style=\\"color:red;\\"><span class=\\"foo bar\\">1 + false</span><span class=\\"foo bar\\">1 + false</span><span class=\\"foo bar\\">1 + false</span><span class=\\"foo bar\\">1 + false</span><span class=\\"foo bar\\">1 + false</span></div>", 1)
])))
}"
`;
exports[`stringify static html > should bail for <option> elements with number values 1`] = ` exports[`stringify static html > should bail for <option> elements with number values 1`] = `
"const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue "const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
const _hoisted_1 = /*#__PURE__*/_createElementVNode("select", null, [
/*#__PURE__*/_createElementVNode("option", { value: 1 }),
/*#__PURE__*/_createElementVNode("option", { value: 1 }),
/*#__PURE__*/_createElementVNode("option", { value: 1 }),
/*#__PURE__*/_createElementVNode("option", { value: 1 }),
/*#__PURE__*/_createElementVNode("option", { value: 1 })
], -1 /* HOISTED */)
const _hoisted_2 = [
_hoisted_1
]
return function render(_ctx, _cache) { return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, _hoisted_2)) return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
_createElementVNode("select", null, [
_createElementVNode("option", { value: 1 }),
_createElementVNode("option", { value: 1 }),
_createElementVNode("option", { value: 1 }),
_createElementVNode("option", { value: 1 }),
_createElementVNode("option", { value: 1 })
], -1 /* CACHED */)
])))
}" }"
`; `;
exports[`stringify static html > should bail on bindings that are hoisted but not stringifiable 1`] = ` exports[`stringify static html > should bail on bindings that are cached but not stringifiable 1`] = `
"const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue "const { createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
const _hoisted_1 = /*#__PURE__*/_createElementVNode("div", null, [
/*#__PURE__*/_createElementVNode("span", { class: "foo" }, "foo"),
/*#__PURE__*/_createElementVNode("span", { class: "foo" }, "foo"),
/*#__PURE__*/_createElementVNode("span", { class: "foo" }, "foo"),
/*#__PURE__*/_createElementVNode("span", { class: "foo" }, "foo"),
/*#__PURE__*/_createElementVNode("span", { class: "foo" }, "foo"),
/*#__PURE__*/_createElementVNode("img", { src: _imports_0_ })
], -1 /* HOISTED */)
const _hoisted_2 = [
_hoisted_1
]
return function render(_ctx, _cache) { return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, _hoisted_2)) return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
_createElementVNode("div", null, [
_createElementVNode("span", { class: "foo" }, "foo"),
_createElementVNode("span", { class: "foo" }, "foo"),
_createElementVNode("span", { class: "foo" }, "foo"),
_createElementVNode("span", { class: "foo" }, "foo"),
_createElementVNode("span", { class: "foo" }, "foo"),
_createElementVNode("img", { src: _imports_0_ })
], -1 /* CACHED */)
])))
}" }"
`; `;
exports[`stringify static html > should work for <option> elements with string values 1`] = ` exports[`stringify static html > should work for <option> elements with string values 1`] = `
"const { createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue "const { createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
const _hoisted_1 = /*#__PURE__*/_createStaticVNode("<select><option value=\\"1\\"></option><option value=\\"1\\"></option><option value=\\"1\\"></option><option value=\\"1\\"></option><option value=\\"1\\"></option></select>", 1) return function render(_ctx, _cache) {
const _hoisted_2 = [ return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
_hoisted_1 _createStaticVNode("<select><option value=\\"1\\"></option><option value=\\"1\\"></option><option value=\\"1\\"></option><option value=\\"1\\"></option><option value=\\"1\\"></option></select>", 1)
] ])))
}"
`;
exports[`stringify static html > should work for multiple adjacent nodes 1`] = `
"const { createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
return function render(_ctx, _cache) { return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, _hoisted_2)) return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
_createStaticVNode("<span class=\\"foo\\"></span><span class=\\"foo\\"></span><span class=\\"foo\\"></span><span class=\\"foo\\"></span><span class=\\"foo\\"></span>", 5)
])))
}"
`;
exports[`stringify static html > should work on eligible content (elements > 20) 1`] = `
"const { createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
_createStaticVNode("<div><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></div>", 1)
])))
}"
`;
exports[`stringify static html > should work on eligible content (elements with binding > 5) 1`] = `
"const { createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
_createStaticVNode("<div><span class=\\"foo\\"></span><span class=\\"foo\\"></span><span class=\\"foo\\"></span><span class=\\"foo\\"></span><span class=\\"foo\\"></span></div>", 1)
])))
}" }"
`; `;
exports[`stringify static html > should work with bindings that are non-static but stringifiable 1`] = ` exports[`stringify static html > should work with bindings that are non-static but stringifiable 1`] = `
"const { createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue "const { createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode, openBlock: _openBlock, createElementBlock: _createElementBlock } = Vue
const _hoisted_1 = /*#__PURE__*/_createStaticVNode("<div><span class=\\"foo\\">foo</span><span class=\\"foo\\">foo</span><span class=\\"foo\\">foo</span><span class=\\"foo\\">foo</span><span class=\\"foo\\">foo</span><img src=\\"" + _imports_0_ + "\\"></div>", 1)
const _hoisted_2 = [
_hoisted_1
]
return function render(_ctx, _cache) { return function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, _hoisted_2)) return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
_createStaticVNode("<div><span class=\\"foo\\">foo</span><span class=\\"foo\\">foo</span><span class=\\"foo\\">foo</span><span class=\\"foo\\">foo</span><span class=\\"foo\\">foo</span><img src=\\"" + _imports_0_ + "\\"></div>", 1)
])))
}" }"
`; `;
exports[`stringify static html > stringify v-html 1`] = ` exports[`stringify static html > stringify v-html 1`] = `
"const { createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode } = Vue "const { createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode } = Vue
const _hoisted_1 = /*#__PURE__*/_createStaticVNode("<pre data-type=\\"js\\"><code><span>show-it </span></code></pre><div class><span class>1</span><span class>2</span></div>", 2)
return function render(_ctx, _cache) { return function render(_ctx, _cache) {
return _hoisted_1 return _cache[0] || (_cache[0] = _createStaticVNode("<pre data-type=\\"js\\"><code><span>show-it </span></code></pre><div class><span class>1</span><span class>2</span></div>", 2))
}" }"
`; `;
exports[`stringify static html > stringify v-text 1`] = ` exports[`stringify static html > stringify v-text 1`] = `
"const { createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode } = Vue "const { createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode } = Vue
const _hoisted_1 = /*#__PURE__*/_createStaticVNode("<pre data-type=\\"js\\"><code>&lt;span&gt;show-it &lt;/span&gt;</code></pre><div class><span class>1</span><span class>2</span></div>", 2)
return function render(_ctx, _cache) { return function render(_ctx, _cache) {
return _hoisted_1 return _cache[0] || (_cache[0] = _createStaticVNode("<pre data-type=\\"js\\"><code>&lt;span&gt;show-it &lt;/span&gt;</code></pre><div class><span class>1</span><span class>2</span></div>", 2))
}" }"
`; `;
exports[`stringify static html > stringify v-text with escape 1`] = ` exports[`stringify static html > stringify v-text with escape 1`] = `
"const { createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode } = Vue "const { createElementVNode: _createElementVNode, createStaticVNode: _createStaticVNode } = Vue
const _hoisted_1 = /*#__PURE__*/_createStaticVNode("<pre data-type=\\"js\\"><code>text1</code></pre><div class><span class>1</span><span class>2</span></div>", 2)
return function render(_ctx, _cache) { return function render(_ctx, _cache) {
return _hoisted_1 return _cache[0] || (_cache[0] = _createStaticVNode("<pre data-type=\\"js\\"><code>text1</code></pre><div class><span class>1</span><span class>2</span></div>", 2))
}" }"
`; `;

View File

@ -23,134 +23,147 @@ describe('stringify static html', () => {
return code.repeat(n) return code.repeat(n)
} }
/**
* Assert cached node NOT stringified
*/
function cachedArrayBailedMatcher(n = 1) {
return {
type: NodeTypes.JS_CACHE_EXPRESSION,
value: {
type: NodeTypes.JS_ARRAY_EXPRESSION,
elements: new Array(n).fill(0).map(() => ({
// should remain VNODE_CALL instead of JS_CALL_EXPRESSION
codegenNode: { type: NodeTypes.VNODE_CALL },
})),
},
}
}
/**
* Assert cached node is stringified (no content check)
*/
function cachedArraySuccessMatcher(n = 1) {
return {
type: NodeTypes.JS_CACHE_EXPRESSION,
value: {
type: NodeTypes.JS_ARRAY_EXPRESSION,
elements: new Array(n).fill(0).map(() => ({
type: NodeTypes.JS_CALL_EXPRESSION,
callee: CREATE_STATIC,
})),
},
}
}
/**
* Assert cached node stringified with desired content and node count
*/
function cachedArrayStaticNodeMatcher(content: string, count: number) {
return {
type: NodeTypes.JS_CACHE_EXPRESSION,
value: {
type: NodeTypes.JS_ARRAY_EXPRESSION,
elements: [
{
type: NodeTypes.JS_CALL_EXPRESSION,
callee: CREATE_STATIC,
arguments: [JSON.stringify(content), String(count)],
},
],
},
}
}
test('should bail on non-eligible static trees', () => { test('should bail on non-eligible static trees', () => {
const { ast } = compileWithStringify( const { ast } = compileWithStringify(
`<div><div><div>hello</div><div>hello</div></div></div>`, `<div><div><div>hello</div><div>hello</div></div></div>`,
) )
// should be a normal vnode call // should be cached children array
expect(ast.hoists[0]!.type).toBe(NodeTypes.VNODE_CALL) expect(ast.cached[0]!.value.type).toBe(NodeTypes.JS_ARRAY_EXPRESSION)
}) })
test('should work on eligible content (elements with binding > 5)', () => { test('should work on eligible content (elements with binding > 5)', () => {
const { ast } = compileWithStringify( const { code, ast } = compileWithStringify(
`<div><div>${repeat( `<div><div>${repeat(
`<span class="foo"/>`, `<span class="foo"/>`,
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT, StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
)}</div></div>`, )}</div></div>`,
) )
// should be optimized now // should be optimized now
expect(ast.hoists).toMatchObject([ expect(ast.cached).toMatchObject([
{ cachedArrayStaticNodeMatcher(
type: NodeTypes.JS_CALL_EXPRESSION,
callee: CREATE_STATIC,
arguments: [
JSON.stringify(
`<div>${repeat( `<div>${repeat(
`<span class="foo"></span>`, `<span class="foo"></span>`,
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT, StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
)}</div>`, )}</div>`,
1,
), ),
'1',
],
}, // the children array is hoisted as well
{
type: NodeTypes.JS_ARRAY_EXPRESSION,
},
]) ])
expect(code).toMatchSnapshot()
}) })
test('should work on eligible content (elements > 20)', () => { test('should work on eligible content (elements > 20)', () => {
const { ast } = compileWithStringify( const { code, ast } = compileWithStringify(
`<div><div>${repeat( `<div><div>${repeat(
`<span/>`, `<span/>`,
StringifyThresholds.NODE_COUNT, StringifyThresholds.NODE_COUNT,
)}</div></div>`, )}</div></div>`,
) )
// should be optimized now // should be optimized now
expect(ast.hoists).toMatchObject([ expect(ast.cached).toMatchObject([
{ cachedArrayStaticNodeMatcher(
type: NodeTypes.JS_CALL_EXPRESSION, `<div>${repeat(`<span></span>`, StringifyThresholds.NODE_COUNT)}</div>`,
callee: CREATE_STATIC, 1,
arguments: [
JSON.stringify(
`<div>${repeat(
`<span></span>`,
StringifyThresholds.NODE_COUNT,
)}</div>`,
), ),
'1',
],
},
// the children array is hoisted as well
{
type: NodeTypes.JS_ARRAY_EXPRESSION,
},
]) ])
expect(code).toMatchSnapshot()
}) })
test('should work for multiple adjacent nodes', () => { test('should work for multiple adjacent nodes', () => {
const { ast } = compileWithStringify( const { ast, code } = compileWithStringify(
`<div>${repeat( `<div>${repeat(
`<span class="foo"/>`, `<span class="foo"/>`,
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT, StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
)}</div>`, )}</div>`,
) )
// should have 6 hoisted nodes (including the entire array), expect(ast.cached).toMatchObject([
// but 2~5 should be null because they are merged into 1 cachedArrayStaticNodeMatcher(
expect(ast.hoists).toMatchObject([
{
type: NodeTypes.JS_CALL_EXPRESSION,
callee: CREATE_STATIC,
arguments: [
JSON.stringify(
repeat( repeat(
`<span class="foo"></span>`, `<span class="foo"></span>`,
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT, StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
), ),
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
), ),
'5',
],
},
null,
null,
null,
null,
{
type: NodeTypes.JS_ARRAY_EXPRESSION,
},
]) ])
expect(code).toMatchSnapshot()
}) })
test('serializing constant bindings', () => { test('serializing constant bindings', () => {
const { ast } = compileWithStringify( const { ast, code } = compileWithStringify(
`<div><div :style="{ color: 'red' }">${repeat( `<div><div :style="{ color: 'red' }">${repeat(
`<span :class="[{ foo: true }, { bar: true }]">{{ 1 }} + {{ false }}</span>`, `<span :class="[{ foo: true }, { bar: true }]">{{ 1 }} + {{ false }}</span>`,
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT, StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
)}</div></div>`, )}</div></div>`,
) )
// should be optimized now // should be optimized now
expect(ast.hoists).toMatchObject([ expect(ast.cached).toMatchObject([
{ cachedArrayStaticNodeMatcher(
type: NodeTypes.JS_CALL_EXPRESSION,
callee: CREATE_STATIC,
arguments: [
JSON.stringify(
`<div style="color:red;">${repeat( `<div style="color:red;">${repeat(
`<span class="foo bar">1 + false</span>`, `<span class="foo bar">1 + false</span>`,
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT, StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
)}</div>`, )}</div>`,
1,
), ),
'1',
],
},
{
type: NodeTypes.JS_ARRAY_EXPRESSION,
},
]) ])
expect(code).toMatchSnapshot()
}) })
test('escape', () => { test('escape', () => {
const { ast } = compileWithStringify( const { ast, code } = compileWithStringify(
`<div><div>${repeat( `<div><div>${repeat(
`<span :class="'foo' + '&gt;ar'">{{ 1 }} + {{ '<' }}</span>` + `<span :class="'foo' + '&gt;ar'">{{ 1 }} + {{ '<' }}</span>` +
`<span>&amp;</span>`, `<span>&amp;</span>`,
@ -158,27 +171,19 @@ describe('stringify static html', () => {
)}</div></div>`, )}</div></div>`,
) )
// should be optimized now // should be optimized now
expect(ast.hoists).toMatchObject([ expect(ast.cached).toMatchObject([
{ cachedArrayStaticNodeMatcher(
type: NodeTypes.JS_CALL_EXPRESSION,
callee: CREATE_STATIC,
arguments: [
JSON.stringify(
`<div>${repeat( `<div>${repeat(
`<span class="foo&gt;ar">1 + &lt;</span>` + `<span>&amp;</span>`, `<span class="foo&gt;ar">1 + &lt;</span>` + `<span>&amp;</span>`,
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT, StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
)}</div>`, )}</div>`,
1,
), ),
'1',
],
},
{
type: NodeTypes.JS_ARRAY_EXPRESSION,
},
]) ])
expect(code).toMatchSnapshot()
}) })
test('should bail on bindings that are hoisted but not stringifiable', () => { test('should bail on bindings that are cached but not stringifiable', () => {
const { ast, code } = compile( const { ast, code } = compile(
`<div><div>${repeat( `<div><div>${repeat(
`<span class="foo">foo</span>`, `<span class="foo">foo</span>`,
@ -195,7 +200,7 @@ describe('stringify static html', () => {
'_imports_0_', '_imports_0_',
false, false,
node.loc, node.loc,
ConstantTypes.CAN_HOIST, ConstantTypes.CAN_CACHE,
) )
node.props[0] = { node.props[0] = {
type: NodeTypes.DIRECTIVE, type: NodeTypes.DIRECTIVE,
@ -210,17 +215,7 @@ describe('stringify static html', () => {
], ],
}, },
) )
expect(ast.hoists).toMatchObject([ expect(ast.cached).toMatchObject([cachedArrayBailedMatcher()])
{
// the expression and the tree are still hoistable
// but should stay NodeTypes.VNODE_CALL
// if it's stringified it will be NodeTypes.JS_CALL_EXPRESSION
type: NodeTypes.VNODE_CALL,
},
{
type: NodeTypes.JS_ARRAY_EXPRESSION,
},
])
expect(code).toMatchSnapshot() expect(code).toMatchSnapshot()
}) })
@ -258,35 +253,19 @@ describe('stringify static html', () => {
], ],
}, },
) )
expect(ast.hoists).toMatchObject([ expect(ast.cached).toMatchObject([cachedArraySuccessMatcher()])
{
// the hoisted node should be NodeTypes.JS_CALL_EXPRESSION
// of `createStaticVNode()` instead of dynamic NodeTypes.VNODE_CALL
type: NodeTypes.JS_CALL_EXPRESSION,
},
{
type: NodeTypes.JS_ARRAY_EXPRESSION,
},
])
expect(code).toMatchSnapshot() expect(code).toMatchSnapshot()
}) })
// #1128 // #1128
test('should bail on non attribute bindings', () => { test('should bail on non-attribute bindings', () => {
const { ast } = compileWithStringify( const { ast } = compileWithStringify(
`<div><div><input indeterminate>${repeat( `<div><div><input indeterminate>${repeat(
`<span class="foo">foo</span>`, `<span class="foo">foo</span>`,
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT, StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
)}</div></div>`, )}</div></div>`,
) )
expect(ast.hoists).toMatchObject([ expect(ast.cached).toMatchObject([cachedArrayBailedMatcher()])
{
type: NodeTypes.VNODE_CALL, // not CALL_EXPRESSION
},
{
type: NodeTypes.JS_ARRAY_EXPRESSION,
},
])
const { ast: ast2 } = compileWithStringify( const { ast: ast2 } = compileWithStringify(
`<div><div><input :indeterminate="true">${repeat( `<div><div><input :indeterminate="true">${repeat(
@ -294,46 +273,23 @@ describe('stringify static html', () => {
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT, StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
)}</div></div>`, )}</div></div>`,
) )
expect(ast2.hoists).toMatchObject([ expect(ast2.cached).toMatchObject([cachedArrayBailedMatcher()])
{
type: NodeTypes.VNODE_CALL, // not CALL_EXPRESSION
},
{
type: NodeTypes.JS_ARRAY_EXPRESSION,
},
])
})
test('should bail on non attribute bindings', () => { const { ast: ast3 } = compileWithStringify(
const { ast } = compileWithStringify(
`<div><div>${repeat( `<div><div>${repeat(
`<span class="foo">foo</span>`, `<span class="foo">foo</span>`,
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT, StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
)}<input indeterminate></div></div>`, )}<input indeterminate></div></div>`,
) )
expect(ast.hoists).toMatchObject([ expect(ast3.cached).toMatchObject([cachedArrayBailedMatcher()])
{
type: NodeTypes.VNODE_CALL, // not CALL_EXPRESSION
},
{
type: NodeTypes.JS_ARRAY_EXPRESSION,
},
])
const { ast: ast2 } = compileWithStringify( const { ast: ast4 } = compileWithStringify(
`<div><div>${repeat( `<div><div>${repeat(
`<span class="foo">foo</span>`, `<span class="foo">foo</span>`,
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT, StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
)}<input :indeterminate="true"></div></div>`, )}<input :indeterminate="true"></div></div>`,
) )
expect(ast2.hoists).toMatchObject([ expect(ast4.cached).toMatchObject([cachedArrayBailedMatcher()])
{
type: NodeTypes.VNODE_CALL, // not CALL_EXPRESSION
},
{
type: NodeTypes.JS_ARRAY_EXPRESSION,
},
])
}) })
test('should bail on tags that has placement constraints (eg.tables related tags)', () => { test('should bail on tags that has placement constraints (eg.tables related tags)', () => {
@ -343,14 +299,7 @@ describe('stringify static html', () => {
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT, StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
)}</tbody></table>`, )}</tbody></table>`,
) )
expect(ast.hoists).toMatchObject([ expect(ast.cached).toMatchObject([cachedArrayBailedMatcher()])
{
type: NodeTypes.VNODE_CALL, // not CALL_EXPRESSION
},
{
type: NodeTypes.JS_ARRAY_EXPRESSION,
},
])
}) })
test('should bail inside slots', () => { test('should bail inside slots', () => {
@ -360,14 +309,9 @@ describe('stringify static html', () => {
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT, StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
)}</foo>`, )}</foo>`,
) )
expect(ast.hoists.length).toBe( expect(ast.cached).toMatchObject([
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT, cachedArrayBailedMatcher(StringifyThresholds.ELEMENT_WITH_BINDING_COUNT),
) ])
ast.hoists.forEach(node => {
expect(node).toMatchObject({
type: NodeTypes.VNODE_CALL, // not CALL_EXPRESSION
})
})
const { ast: ast2 } = compileWithStringify( const { ast: ast2 } = compileWithStringify(
`<foo><template #foo>${repeat( `<foo><template #foo>${repeat(
@ -375,14 +319,9 @@ describe('stringify static html', () => {
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT, StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
)}</template></foo>`, )}</template></foo>`,
) )
expect(ast2.hoists.length).toBe( expect(ast2.cached).toMatchObject([
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT, cachedArrayBailedMatcher(StringifyThresholds.ELEMENT_WITH_BINDING_COUNT),
) ])
ast2.hoists.forEach(node => {
expect(node).toMatchObject({
type: NodeTypes.VNODE_CALL, // not CALL_EXPRESSION
})
})
}) })
test('should remove attribute for `null`', () => { test('should remove attribute for `null`', () => {
@ -392,19 +331,13 @@ describe('stringify static html', () => {
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT, StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
)}</div>`, )}</div>`,
) )
expect(ast.hoists[0]).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION, expect(ast.cached).toMatchObject([
callee: CREATE_STATIC, cachedArrayStaticNodeMatcher(
arguments: [ repeat(`<span></span>`, StringifyThresholds.ELEMENT_WITH_BINDING_COUNT),
JSON.stringify(
`${repeat(
`<span></span>`,
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT, StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
)}`,
), ),
'5', ])
],
})
}) })
// #6617 // #6617
@ -415,7 +348,10 @@ describe('stringify static html', () => {
StringifyThresholds.NODE_COUNT, StringifyThresholds.NODE_COUNT,
)}`, )}`,
) )
expect(ast.hoists[0]).toMatchObject({ expect(ast.cached).toMatchObject([
{
type: NodeTypes.JS_CACHE_EXPRESSION,
value: {
type: NodeTypes.JS_CALL_EXPRESSION, type: NodeTypes.JS_CALL_EXPRESSION,
callee: CREATE_STATIC, callee: CREATE_STATIC,
arguments: [ arguments: [
@ -427,7 +363,9 @@ describe('stringify static html', () => {
), ),
'21', '21',
], ],
}) },
},
])
}) })
test('should stringify svg', () => { test('should stringify svg', () => {
@ -439,19 +377,16 @@ describe('stringify static html', () => {
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT, StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
)}</svg></div>`, )}</svg></div>`,
) )
expect(ast.hoists[0]).toMatchObject({
type: NodeTypes.JS_CALL_EXPRESSION, expect(ast.cached).toMatchObject([
callee: CREATE_STATIC, cachedArrayStaticNodeMatcher(
arguments: [
JSON.stringify(
`${svg}${repeat( `${svg}${repeat(
repeated, repeated,
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT, StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
)}</svg>`, )}</svg>`,
1,
), ),
'1', ])
],
})
}) })
// #5439 // #5439
@ -494,23 +429,14 @@ describe('stringify static html', () => {
)}</select></div>`, )}</select></div>`,
) )
// should be optimized now // should be optimized now
expect(ast.hoists).toMatchObject([ expect(ast.cached).toMatchObject([
{ cachedArrayStaticNodeMatcher(
type: NodeTypes.JS_CALL_EXPRESSION,
callee: CREATE_STATIC,
arguments: [
JSON.stringify(
`<select>${repeat( `<select>${repeat(
`<option value="1"></option>`, `<option value="1"></option>`,
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT, StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
)}</select>`, )}</select>`,
1,
), ),
'1',
],
},
{
type: NodeTypes.JS_ARRAY_EXPRESSION,
},
]) ])
expect(code).toMatchSnapshot() expect(code).toMatchSnapshot()
}) })
@ -522,14 +448,7 @@ describe('stringify static html', () => {
StringifyThresholds.ELEMENT_WITH_BINDING_COUNT, StringifyThresholds.ELEMENT_WITH_BINDING_COUNT,
)}</select></div>`, )}</select></div>`,
) )
expect(ast.hoists).toMatchObject([ expect(ast.cached).toMatchObject([cachedArrayBailedMatcher()])
{
type: NodeTypes.VNODE_CALL,
},
{
type: NodeTypes.JS_ARRAY_EXPRESSION,
},
])
expect(code).toMatchSnapshot() expect(code).toMatchSnapshot()
}) })
}) })

View File

@ -268,7 +268,7 @@ describe('compiler-dom: transform v-on', () => {
prefixIdentifiers: true, prefixIdentifiers: true,
cacheHandlers: true, cacheHandlers: true,
}) })
expect(root.cached).toBe(1) expect(root.cached.length).toBe(1)
// should not treat cached handler as dynamicProp, so it should have no // should not treat cached handler as dynamicProp, so it should have no
// dynamicProps flags and only the hydration flag // dynamicProps flags and only the hydration flag
expect((root as any).children[0].codegenNode.patchFlag).toBe( expect((root as any).children[0].codegenNode.patchFlag).toBe(

View File

@ -3,12 +3,12 @@
*/ */
import { import {
CREATE_STATIC, CREATE_STATIC,
type CacheExpression,
ConstantTypes, ConstantTypes,
type ElementNode, type ElementNode,
ElementTypes, ElementTypes,
type ExpressionNode, type ExpressionNode,
type HoistTransform, type HoistTransform,
type JSChildNode,
Namespaces, Namespaces,
NodeTypes, NodeTypes,
type PlainElementNode, type PlainElementNode,
@ -16,11 +16,14 @@ import {
type TemplateChildNode, type TemplateChildNode,
type TextCallNode, type TextCallNode,
type TransformContext, type TransformContext,
type VNodeCall,
createArrayExpression,
createCallExpression, createCallExpression,
isStaticArgOf, isStaticArgOf,
} from '@vue/compiler-core' } from '@vue/compiler-core'
import { import {
escapeHtml, escapeHtml,
isArray,
isBooleanAttr, isBooleanAttr,
isKnownHtmlAttr, isKnownHtmlAttr,
isKnownSvgAttr, isKnownSvgAttr,
@ -76,6 +79,14 @@ export const stringifyStatic: HoistTransform = (children, context, parent) => {
return return
} }
const isParentCached =
parent.type === NodeTypes.ELEMENT &&
parent.codegenNode &&
parent.codegenNode.type === NodeTypes.VNODE_CALL &&
parent.codegenNode.children &&
!isArray(parent.codegenNode.children) &&
parent.codegenNode.children.type === NodeTypes.JS_CACHE_EXPRESSION
let nc = 0 // current node count let nc = 0 // current node count
let ec = 0 // current element with binding count let ec = 0 // current element with binding count
const currentChunk: StringifiableNode[] = [] const currentChunk: StringifiableNode[] = []
@ -94,37 +105,48 @@ export const stringifyStatic: HoistTransform = (children, context, parent) => {
// will insert / hydrate // will insert / hydrate
String(currentChunk.length), String(currentChunk.length),
]) ])
if (isParentCached) {
;((parent.codegenNode as VNodeCall).children as CacheExpression).value =
createArrayExpression([staticCall])
} else {
// replace the first node's hoisted expression with the static vnode call // replace the first node's hoisted expression with the static vnode call
replaceHoist(currentChunk[0], staticCall, context) ;(currentChunk[0].codegenNode as CacheExpression).value = staticCall
if (currentChunk.length > 1) { if (currentChunk.length > 1) {
for (let i = 1; i < currentChunk.length; i++) { // remove merged nodes from children
// for the merged nodes, set their hoisted expression to null
replaceHoist(currentChunk[i], null, context)
}
// also remove merged nodes from children
const deleteCount = currentChunk.length - 1 const deleteCount = currentChunk.length - 1
children.splice(currentIndex - currentChunk.length + 1, deleteCount) children.splice(currentIndex - currentChunk.length + 1, deleteCount)
// also adjust index for the remaining cache items
const cacheIndex = context.cached.indexOf(
currentChunk[currentChunk.length - 1]
.codegenNode as CacheExpression,
)
if (cacheIndex > -1) {
for (let i = cacheIndex; i < context.cached.length; i++) {
const c = context.cached[i]
if (c) c.index -= deleteCount
}
context.cached.splice(cacheIndex - deleteCount + 1, deleteCount)
}
return deleteCount return deleteCount
} }
} }
}
return 0 return 0
} }
let i = 0 let i = 0
for (; i < children.length; i++) { for (; i < children.length; i++) {
const child = children[i] const child = children[i]
const hoisted = getHoistedNode(child) const isCached = isParentCached || getCachedNode(child)
if (hoisted) { if (isCached) {
// presence of hoisted means child must be a stringifiable node // presence of cached means child must be a stringifiable node
const node = child as StringifiableNode const result = analyzeNode(child as StringifiableNode)
const result = analyzeNode(node)
if (result) { if (result) {
// node is stringifiable, record state // node is stringifiable, record state
nc += result[0] nc += result[0]
ec += result[1] ec += result[1]
currentChunk.push(node) currentChunk.push(child as StringifiableNode)
continue continue
} }
} }
@ -141,12 +163,19 @@ export const stringifyStatic: HoistTransform = (children, context, parent) => {
stringifyCurrentChunk(i) stringifyCurrentChunk(i)
} }
const getHoistedNode = (node: TemplateChildNode) => const getCachedNode = (
((node.type === NodeTypes.ELEMENT && node.tagType === ElementTypes.ELEMENT) || node: TemplateChildNode,
node.type == NodeTypes.TEXT_CALL) && ): CacheExpression | undefined => {
if (
((node.type === NodeTypes.ELEMENT &&
node.tagType === ElementTypes.ELEMENT) ||
node.type === NodeTypes.TEXT_CALL) &&
node.codegenNode && node.codegenNode &&
node.codegenNode.type === NodeTypes.SIMPLE_EXPRESSION && node.codegenNode.type === NodeTypes.JS_CACHE_EXPRESSION
node.codegenNode.hoisted ) {
return node.codegenNode
}
}
const dataAriaRE = /^(data|aria)-/ const dataAriaRE = /^(data|aria)-/
const isStringifiableAttr = (name: string, ns: Namespaces) => { const isStringifiableAttr = (name: string, ns: Namespaces) => {
@ -159,21 +188,12 @@ const isStringifiableAttr = (name: string, ns: Namespaces) => {
) )
} }
const replaceHoist = (
node: StringifiableNode,
replacement: JSChildNode | null,
context: TransformContext,
) => {
const hoistToReplace = (node.codegenNode as SimpleExpressionNode).hoisted!
context.hoists[context.hoists.indexOf(hoistToReplace)] = replacement
}
const isNonStringifiable = /*#__PURE__*/ makeMap( const isNonStringifiable = /*#__PURE__*/ makeMap(
`caption,thead,tr,th,tbody,td,tfoot,colgroup,col`, `caption,thead,tr,th,tbody,td,tfoot,colgroup,col`,
) )
/** /**
* for a hoisted node, analyze it and return: * for a cached node, analyze it and return:
* - false: bailed (contains non-stringifiable props or runtime constant) * - false: bailed (contains non-stringifiable props or runtime constant)
* - [nc, ec] where * - [nc, ec] where
* - nc is the number of nodes inside * - nc is the number of nodes inside
@ -381,7 +401,7 @@ function evaluateConstant(exp: ExpressionNode): string {
} else if (c.type === NodeTypes.INTERPOLATION) { } else if (c.type === NodeTypes.INTERPOLATION) {
res += toDisplayString(evaluateConstant(c.content)) res += toDisplayString(evaluateConstant(c.content))
} else { } else {
res += evaluateConstant(c) res += evaluateConstant(c as ExpressionNode)
} }
}) })
return res return res

View File

@ -851,8 +851,6 @@ return (_ctx, _cache) => {
exports[`SFC compile <script setup> > inlineTemplate mode > should work 1`] = ` exports[`SFC compile <script setup> > inlineTemplate mode > should work 1`] = `
"import { toDisplayString as _toDisplayString, createElementVNode as _createElementVNode, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue" "import { toDisplayString as _toDisplayString, createElementVNode as _createElementVNode, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
const _hoisted_1 = /*#__PURE__*/_createElementVNode("div", null, "static", -1 /* HOISTED */)
import { ref } from 'vue' import { ref } from 'vue'
export default { export default {
@ -863,7 +861,7 @@ export default {
return (_ctx, _cache) => { return (_ctx, _cache) => {
return (_openBlock(), _createElementBlock(_Fragment, null, [ return (_openBlock(), _createElementBlock(_Fragment, null, [
_createElementVNode("div", null, _toDisplayString(count.value), 1 /* TEXT */), _createElementVNode("div", null, _toDisplayString(count.value), 1 /* TEXT */),
_hoisted_1 _cache[0] || (_cache[0] = _createElementVNode("div", null, "static", -1 /* CACHED */))
], 64 /* STABLE_FRAGMENT */)) ], 64 /* STABLE_FRAGMENT */))
} }
} }

View File

@ -38,13 +38,11 @@ import _imports_0 from '@svg/file.svg'
const _hoisted_1 = _imports_0 + '#fragment' const _hoisted_1 = _imports_0 + '#fragment'
const _hoisted_2 = /*#__PURE__*/_createElementVNode("use", { href: _hoisted_1 }, null, -1 /* HOISTED */)
const _hoisted_3 = /*#__PURE__*/_createElementVNode("use", { href: _hoisted_1 }, null, -1 /* HOISTED */)
export function render(_ctx, _cache) { export function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock(_Fragment, null, [ return (_openBlock(), _createElementBlock(_Fragment, null, [
_hoisted_2, _cache[0] || (_cache[0] = _createElementVNode("use", { href: _hoisted_1 }, null, -1 /* CACHED */)),
_hoisted_3 _cache[1] || (_cache[1] = _createElementVNode("use", { href: _hoisted_1 }, null, -1 /* CACHED */))
], 64 /* STABLE_FRAGMENT */)) ], 64 /* STABLE_FRAGMENT */))
}" }"
`; `;
@ -82,13 +80,10 @@ import _imports_0 from './bar.png'
import _imports_1 from '/bar.png' import _imports_1 from '/bar.png'
const _hoisted_1 = /*#__PURE__*/_createStaticVNode("<img src=\\"" + _imports_0 + "\\"><img src=\\"" + _imports_1 + "\\"><img src=\\"https://foo.bar/baz.png\\"><img src=\\"//foo.bar/baz.png\\"><img src=\\"" + _imports_0 + "\\">", 5)
const _hoisted_6 = [
_hoisted_1
]
export function render(_ctx, _cache) { export function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, _hoisted_6)) return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
_createStaticVNode("<img src=\\"" + _imports_0 + "\\"><img src=\\"" + _imports_1 + "\\"><img src=\\"https://foo.bar/baz.png\\"><img src=\\"//foo.bar/baz.png\\"><img src=\\"" + _imports_0 + "\\">", 5)
])))
}" }"
`; `;

View File

@ -7,13 +7,11 @@ import _imports_0 from '@/logo.png'
const _hoisted_1 = _imports_0 + ', ' + _imports_0 + ' 2x' const _hoisted_1 = _imports_0 + ', ' + _imports_0 + ' 2x'
const _hoisted_2 = _imports_0 + ' 1x, ' + "/foo/logo.png" + ' 2x' const _hoisted_2 = _imports_0 + ' 1x, ' + "/foo/logo.png" + ' 2x'
const _hoisted_3 = /*#__PURE__*/_createElementVNode("img", { srcset: _hoisted_1 }, null, -1 /* HOISTED */)
const _hoisted_4 = /*#__PURE__*/_createElementVNode("img", { srcset: _hoisted_2 }, null, -1 /* HOISTED */)
export function render(_ctx, _cache) { export function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock(_Fragment, null, [ return (_openBlock(), _createElementBlock(_Fragment, null, [
_hoisted_3, _cache[0] || (_cache[0] = _createElementVNode("img", { srcset: _hoisted_1 }, null, -1 /* CACHED */)),
_hoisted_4 _cache[1] || (_cache[1] = _createElementVNode("img", { srcset: _hoisted_2 }, null, -1 /* CACHED */))
], 64 /* STABLE_FRAGMENT */)) ], 64 /* STABLE_FRAGMENT */))
}" }"
`; `;
@ -31,69 +29,57 @@ const _hoisted_5 = _imports_0 + ' 2x, ' + _imports_0
const _hoisted_6 = _imports_0 + ' 2x, ' + _imports_0 + ' 3x' const _hoisted_6 = _imports_0 + ' 2x, ' + _imports_0 + ' 3x'
const _hoisted_7 = _imports_0 + ', ' + _imports_0 + ' 2x, ' + _imports_0 + ' 3x' const _hoisted_7 = _imports_0 + ', ' + _imports_0 + ' 2x, ' + _imports_0 + ' 3x'
const _hoisted_8 = "/logo.png" + ', ' + _imports_0 + ' 2x' const _hoisted_8 = "/logo.png" + ', ' + _imports_0 + ' 2x'
const _hoisted_9 = /*#__PURE__*/_createElementVNode("img", {
src: "./logo.png",
srcset: ""
}, null, -1 /* HOISTED */)
const _hoisted_10 = /*#__PURE__*/_createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_1
}, null, -1 /* HOISTED */)
const _hoisted_11 = /*#__PURE__*/_createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_2
}, null, -1 /* HOISTED */)
const _hoisted_12 = /*#__PURE__*/_createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_3
}, null, -1 /* HOISTED */)
const _hoisted_13 = /*#__PURE__*/_createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_4
}, null, -1 /* HOISTED */)
const _hoisted_14 = /*#__PURE__*/_createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_5
}, null, -1 /* HOISTED */)
const _hoisted_15 = /*#__PURE__*/_createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_6
}, null, -1 /* HOISTED */)
const _hoisted_16 = /*#__PURE__*/_createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_7
}, null, -1 /* HOISTED */)
const _hoisted_17 = /*#__PURE__*/_createElementVNode("img", {
src: "/logo.png",
srcset: "/logo.png, /logo.png 2x"
}, null, -1 /* HOISTED */)
const _hoisted_18 = /*#__PURE__*/_createElementVNode("img", {
src: "https://example.com/logo.png",
srcset: "https://example.com/logo.png, https://example.com/logo.png 2x"
}, null, -1 /* HOISTED */)
const _hoisted_19 = /*#__PURE__*/_createElementVNode("img", {
src: "/logo.png",
srcset: _hoisted_8
}, null, -1 /* HOISTED */)
const _hoisted_20 = /*#__PURE__*/_createElementVNode("img", {
src: "data:image/png;base64,i",
srcset: "data:image/png;base64,i 1x, data:image/png;base64,i 2x"
}, null, -1 /* HOISTED */)
export function render(_ctx, _cache) { export function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock(_Fragment, null, [ return (_openBlock(), _createElementBlock(_Fragment, null, [
_hoisted_9, _cache[0] || (_cache[0] = _createElementVNode("img", {
_hoisted_10, src: "./logo.png",
_hoisted_11, srcset: ""
_hoisted_12, }, null, -1 /* CACHED */)),
_hoisted_13, _cache[1] || (_cache[1] = _createElementVNode("img", {
_hoisted_14, src: "./logo.png",
_hoisted_15, srcset: _hoisted_1
_hoisted_16, }, null, -1 /* CACHED */)),
_hoisted_17, _cache[2] || (_cache[2] = _createElementVNode("img", {
_hoisted_18, src: "./logo.png",
_hoisted_19, srcset: _hoisted_2
_hoisted_20 }, null, -1 /* CACHED */)),
_cache[3] || (_cache[3] = _createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_3
}, null, -1 /* CACHED */)),
_cache[4] || (_cache[4] = _createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_4
}, null, -1 /* CACHED */)),
_cache[5] || (_cache[5] = _createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_5
}, null, -1 /* CACHED */)),
_cache[6] || (_cache[6] = _createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_6
}, null, -1 /* CACHED */)),
_cache[7] || (_cache[7] = _createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_7
}, null, -1 /* CACHED */)),
_cache[8] || (_cache[8] = _createElementVNode("img", {
src: "/logo.png",
srcset: "/logo.png, /logo.png 2x"
}, null, -1 /* CACHED */)),
_cache[9] || (_cache[9] = _createElementVNode("img", {
src: "https://example.com/logo.png",
srcset: "https://example.com/logo.png, https://example.com/logo.png 2x"
}, null, -1 /* CACHED */)),
_cache[10] || (_cache[10] = _createElementVNode("img", {
src: "/logo.png",
srcset: _hoisted_8
}, null, -1 /* CACHED */)),
_cache[11] || (_cache[11] = _createElementVNode("img", {
src: "data:image/png;base64,i",
srcset: "data:image/png;base64,i 1x, data:image/png;base64,i 2x"
}, null, -1 /* CACHED */))
], 64 /* STABLE_FRAGMENT */)) ], 64 /* STABLE_FRAGMENT */))
}" }"
`; `;
@ -101,69 +87,56 @@ export function render(_ctx, _cache) {
exports[`compiler sfc: transform srcset > transform srcset w/ base 1`] = ` exports[`compiler sfc: transform srcset > transform srcset w/ base 1`] = `
"import { createElementVNode as _createElementVNode, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue" "import { createElementVNode as _createElementVNode, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
const _hoisted_1 = /*#__PURE__*/_createElementVNode("img", {
src: "./logo.png",
srcset: ""
}, null, -1 /* HOISTED */)
const _hoisted_2 = /*#__PURE__*/_createElementVNode("img", {
src: "./logo.png",
srcset: "/foo/logo.png"
}, null, -1 /* HOISTED */)
const _hoisted_3 = /*#__PURE__*/_createElementVNode("img", {
src: "./logo.png",
srcset: "/foo/logo.png 2x"
}, null, -1 /* HOISTED */)
const _hoisted_4 = /*#__PURE__*/_createElementVNode("img", {
src: "./logo.png",
srcset: "/foo/logo.png 2x"
}, null, -1 /* HOISTED */)
const _hoisted_5 = /*#__PURE__*/_createElementVNode("img", {
src: "./logo.png",
srcset: "/foo/logo.png, /foo/logo.png 2x"
}, null, -1 /* HOISTED */)
const _hoisted_6 = /*#__PURE__*/_createElementVNode("img", {
src: "./logo.png",
srcset: "/foo/logo.png 2x, /foo/logo.png"
}, null, -1 /* HOISTED */)
const _hoisted_7 = /*#__PURE__*/_createElementVNode("img", {
src: "./logo.png",
srcset: "/foo/logo.png 2x, /foo/logo.png 3x"
}, null, -1 /* HOISTED */)
const _hoisted_8 = /*#__PURE__*/_createElementVNode("img", {
src: "./logo.png",
srcset: "/foo/logo.png, /foo/logo.png 2x, /foo/logo.png 3x"
}, null, -1 /* HOISTED */)
const _hoisted_9 = /*#__PURE__*/_createElementVNode("img", {
src: "/logo.png",
srcset: "/logo.png, /logo.png 2x"
}, null, -1 /* HOISTED */)
const _hoisted_10 = /*#__PURE__*/_createElementVNode("img", {
src: "https://example.com/logo.png",
srcset: "https://example.com/logo.png, https://example.com/logo.png 2x"
}, null, -1 /* HOISTED */)
const _hoisted_11 = /*#__PURE__*/_createElementVNode("img", {
src: "/logo.png",
srcset: "/logo.png, /foo/logo.png 2x"
}, null, -1 /* HOISTED */)
const _hoisted_12 = /*#__PURE__*/_createElementVNode("img", {
src: "data:image/png;base64,i",
srcset: "data:image/png;base64,i 1x, data:image/png;base64,i 2x"
}, null, -1 /* HOISTED */)
export function render(_ctx, _cache) { export function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock(_Fragment, null, [ return (_openBlock(), _createElementBlock(_Fragment, null, [
_hoisted_1, _cache[0] || (_cache[0] = _createElementVNode("img", {
_hoisted_2, src: "./logo.png",
_hoisted_3, srcset: ""
_hoisted_4, }, null, -1 /* CACHED */)),
_hoisted_5, _cache[1] || (_cache[1] = _createElementVNode("img", {
_hoisted_6, src: "./logo.png",
_hoisted_7, srcset: "/foo/logo.png"
_hoisted_8, }, null, -1 /* CACHED */)),
_hoisted_9, _cache[2] || (_cache[2] = _createElementVNode("img", {
_hoisted_10, src: "./logo.png",
_hoisted_11, srcset: "/foo/logo.png 2x"
_hoisted_12 }, null, -1 /* CACHED */)),
_cache[3] || (_cache[3] = _createElementVNode("img", {
src: "./logo.png",
srcset: "/foo/logo.png 2x"
}, null, -1 /* CACHED */)),
_cache[4] || (_cache[4] = _createElementVNode("img", {
src: "./logo.png",
srcset: "/foo/logo.png, /foo/logo.png 2x"
}, null, -1 /* CACHED */)),
_cache[5] || (_cache[5] = _createElementVNode("img", {
src: "./logo.png",
srcset: "/foo/logo.png 2x, /foo/logo.png"
}, null, -1 /* CACHED */)),
_cache[6] || (_cache[6] = _createElementVNode("img", {
src: "./logo.png",
srcset: "/foo/logo.png 2x, /foo/logo.png 3x"
}, null, -1 /* CACHED */)),
_cache[7] || (_cache[7] = _createElementVNode("img", {
src: "./logo.png",
srcset: "/foo/logo.png, /foo/logo.png 2x, /foo/logo.png 3x"
}, null, -1 /* CACHED */)),
_cache[8] || (_cache[8] = _createElementVNode("img", {
src: "/logo.png",
srcset: "/logo.png, /logo.png 2x"
}, null, -1 /* CACHED */)),
_cache[9] || (_cache[9] = _createElementVNode("img", {
src: "https://example.com/logo.png",
srcset: "https://example.com/logo.png, https://example.com/logo.png 2x"
}, null, -1 /* CACHED */)),
_cache[10] || (_cache[10] = _createElementVNode("img", {
src: "/logo.png",
srcset: "/logo.png, /foo/logo.png 2x"
}, null, -1 /* CACHED */)),
_cache[11] || (_cache[11] = _createElementVNode("img", {
src: "data:image/png;base64,i",
srcset: "data:image/png;base64,i 1x, data:image/png;base64,i 2x"
}, null, -1 /* CACHED */))
], 64 /* STABLE_FRAGMENT */)) ], 64 /* STABLE_FRAGMENT */))
}" }"
`; `;
@ -183,69 +156,57 @@ const _hoisted_6 = _imports_0 + ' 2x, ' + _imports_0 + ' 3x'
const _hoisted_7 = _imports_0 + ', ' + _imports_0 + ' 2x, ' + _imports_0 + ' 3x' const _hoisted_7 = _imports_0 + ', ' + _imports_0 + ' 2x, ' + _imports_0 + ' 3x'
const _hoisted_8 = _imports_1 + ', ' + _imports_1 + ' 2x' const _hoisted_8 = _imports_1 + ', ' + _imports_1 + ' 2x'
const _hoisted_9 = _imports_1 + ', ' + _imports_0 + ' 2x' const _hoisted_9 = _imports_1 + ', ' + _imports_0 + ' 2x'
const _hoisted_10 = /*#__PURE__*/_createElementVNode("img", {
src: "./logo.png",
srcset: ""
}, null, -1 /* HOISTED */)
const _hoisted_11 = /*#__PURE__*/_createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_1
}, null, -1 /* HOISTED */)
const _hoisted_12 = /*#__PURE__*/_createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_2
}, null, -1 /* HOISTED */)
const _hoisted_13 = /*#__PURE__*/_createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_3
}, null, -1 /* HOISTED */)
const _hoisted_14 = /*#__PURE__*/_createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_4
}, null, -1 /* HOISTED */)
const _hoisted_15 = /*#__PURE__*/_createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_5
}, null, -1 /* HOISTED */)
const _hoisted_16 = /*#__PURE__*/_createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_6
}, null, -1 /* HOISTED */)
const _hoisted_17 = /*#__PURE__*/_createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_7
}, null, -1 /* HOISTED */)
const _hoisted_18 = /*#__PURE__*/_createElementVNode("img", {
src: "/logo.png",
srcset: _hoisted_8
}, null, -1 /* HOISTED */)
const _hoisted_19 = /*#__PURE__*/_createElementVNode("img", {
src: "https://example.com/logo.png",
srcset: "https://example.com/logo.png, https://example.com/logo.png 2x"
}, null, -1 /* HOISTED */)
const _hoisted_20 = /*#__PURE__*/_createElementVNode("img", {
src: "/logo.png",
srcset: _hoisted_9
}, null, -1 /* HOISTED */)
const _hoisted_21 = /*#__PURE__*/_createElementVNode("img", {
src: "data:image/png;base64,i",
srcset: "data:image/png;base64,i 1x, data:image/png;base64,i 2x"
}, null, -1 /* HOISTED */)
export function render(_ctx, _cache) { export function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock(_Fragment, null, [ return (_openBlock(), _createElementBlock(_Fragment, null, [
_hoisted_10, _cache[0] || (_cache[0] = _createElementVNode("img", {
_hoisted_11, src: "./logo.png",
_hoisted_12, srcset: ""
_hoisted_13, }, null, -1 /* CACHED */)),
_hoisted_14, _cache[1] || (_cache[1] = _createElementVNode("img", {
_hoisted_15, src: "./logo.png",
_hoisted_16, srcset: _hoisted_1
_hoisted_17, }, null, -1 /* CACHED */)),
_hoisted_18, _cache[2] || (_cache[2] = _createElementVNode("img", {
_hoisted_19, src: "./logo.png",
_hoisted_20, srcset: _hoisted_2
_hoisted_21 }, null, -1 /* CACHED */)),
_cache[3] || (_cache[3] = _createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_3
}, null, -1 /* CACHED */)),
_cache[4] || (_cache[4] = _createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_4
}, null, -1 /* CACHED */)),
_cache[5] || (_cache[5] = _createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_5
}, null, -1 /* CACHED */)),
_cache[6] || (_cache[6] = _createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_6
}, null, -1 /* CACHED */)),
_cache[7] || (_cache[7] = _createElementVNode("img", {
src: "./logo.png",
srcset: _hoisted_7
}, null, -1 /* CACHED */)),
_cache[8] || (_cache[8] = _createElementVNode("img", {
src: "/logo.png",
srcset: _hoisted_8
}, null, -1 /* CACHED */)),
_cache[9] || (_cache[9] = _createElementVNode("img", {
src: "https://example.com/logo.png",
srcset: "https://example.com/logo.png, https://example.com/logo.png 2x"
}, null, -1 /* CACHED */)),
_cache[10] || (_cache[10] = _createElementVNode("img", {
src: "/logo.png",
srcset: _hoisted_9
}, null, -1 /* CACHED */)),
_cache[11] || (_cache[11] = _createElementVNode("img", {
src: "data:image/png;base64,i",
srcset: "data:image/png;base64,i 1x, data:image/png;base64,i 2x"
}, null, -1 /* CACHED */))
], 64 /* STABLE_FRAGMENT */)) ], 64 /* STABLE_FRAGMENT */))
}" }"
`; `;
@ -265,12 +226,10 @@ const _hoisted_6 = _imports_0 + ' 2x, ' + _imports_0 + ' 3x'
const _hoisted_7 = _imports_0 + ', ' + _imports_0 + ' 2x, ' + _imports_0 + ' 3x' const _hoisted_7 = _imports_0 + ', ' + _imports_0 + ' 2x, ' + _imports_0 + ' 3x'
const _hoisted_8 = _imports_1 + ', ' + _imports_1 + ' 2x' const _hoisted_8 = _imports_1 + ', ' + _imports_1 + ' 2x'
const _hoisted_9 = _imports_1 + ', ' + _imports_0 + ' 2x' const _hoisted_9 = _imports_1 + ', ' + _imports_0 + ' 2x'
const _hoisted_10 = /*#__PURE__*/_createStaticVNode("<img src=\\"./logo.png\\" srcset=\\"\\"><img src=\\"./logo.png\\" srcset=\\"" + _hoisted_1 + "\\"><img src=\\"./logo.png\\" srcset=\\"" + _hoisted_2 + "\\"><img src=\\"./logo.png\\" srcset=\\"" + _hoisted_3 + "\\"><img src=\\"./logo.png\\" srcset=\\"" + _hoisted_4 + "\\"><img src=\\"./logo.png\\" srcset=\\"" + _hoisted_5 + "\\"><img src=\\"./logo.png\\" srcset=\\"" + _hoisted_6 + "\\"><img src=\\"./logo.png\\" srcset=\\"" + _hoisted_7 + "\\"><img src=\\"/logo.png\\" srcset=\\"" + _hoisted_8 + "\\"><img src=\\"https://example.com/logo.png\\" srcset=\\"https://example.com/logo.png, https://example.com/logo.png 2x\\"><img src=\\"/logo.png\\" srcset=\\"" + _hoisted_9 + "\\"><img src=\\"data:image/png;base64,i\\" srcset=\\"data:image/png;base64,i 1x, data:image/png;base64,i 2x\\">", 12)
const _hoisted_22 = [
_hoisted_10
]
export function render(_ctx, _cache) { export function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, _hoisted_22)) return (_openBlock(), _createElementBlock("div", null, _cache[0] || (_cache[0] = [
_createStaticVNode("<img src=\\"./logo.png\\" srcset=\\"\\"><img src=\\"./logo.png\\" srcset=\\"" + _hoisted_1 + "\\"><img src=\\"./logo.png\\" srcset=\\"" + _hoisted_2 + "\\"><img src=\\"./logo.png\\" srcset=\\"" + _hoisted_3 + "\\"><img src=\\"./logo.png\\" srcset=\\"" + _hoisted_4 + "\\"><img src=\\"./logo.png\\" srcset=\\"" + _hoisted_5 + "\\"><img src=\\"./logo.png\\" srcset=\\"" + _hoisted_6 + "\\"><img src=\\"./logo.png\\" srcset=\\"" + _hoisted_7 + "\\"><img src=\\"/logo.png\\" srcset=\\"" + _hoisted_8 + "\\"><img src=\\"https://example.com/logo.png\\" srcset=\\"https://example.com/logo.png, https://example.com/logo.png 2x\\"><img src=\\"/logo.png\\" srcset=\\"" + _hoisted_9 + "\\"><img src=\\"data:image/png;base64,i\\" srcset=\\"data:image/png;base64,i 1x, data:image/png;base64,i 2x\\">", 12)
])))
}" }"
`; `;

View File

@ -1122,7 +1122,7 @@ describe('SSR hydration', () => {
'input', 'input',
{ type: 'checkbox', indeterminate: '' }, { type: 'checkbox', indeterminate: '' },
null, null,
PatchFlags.HOISTED, PatchFlags.CACHED,
), ),
) )
expect((container.firstChild as any).indeterminate).toBe(true) expect((container.firstChild as any).indeterminate).toBe(true)

View File

@ -366,7 +366,7 @@ export function createHydrationFunctions(
const forcePatch = type === 'input' || type === 'option' const forcePatch = type === 'input' || type === 'option'
// skip props & children if this is hoisted static nodes // skip props & children if this is hoisted static nodes
// #5405 in dev, always hydrate children for HMR // #5405 in dev, always hydrate children for HMR
if (__DEV__ || forcePatch || patchFlag !== PatchFlags.HOISTED) { if (__DEV__ || forcePatch || patchFlag !== PatchFlags.CACHED) {
if (dirs) { if (dirs) {
invokeDirectiveHook(vnode, null, parentComponent, 'created') invokeDirectiveHook(vnode, null, parentComponent, 'created')
} }

View File

@ -650,7 +650,7 @@ export function cloneVNode<T, U>(
scopeId: vnode.scopeId, scopeId: vnode.scopeId,
slotScopeIds: vnode.slotScopeIds, slotScopeIds: vnode.slotScopeIds,
children: children:
__DEV__ && patchFlag === PatchFlags.HOISTED && isArray(children) __DEV__ && patchFlag === PatchFlags.CACHED && isArray(children)
? (children as VNode[]).map(deepCloneVNode) ? (children as VNode[]).map(deepCloneVNode)
: children, : children,
target: vnode.target, target: vnode.target,
@ -663,7 +663,7 @@ export function cloneVNode<T, U>(
// fast paths only. // fast paths only.
patchFlag: patchFlag:
extraProps && vnode.type !== Fragment extraProps && vnode.type !== Fragment
? patchFlag === PatchFlags.HOISTED // hoisted node ? patchFlag === PatchFlags.CACHED // hoisted node
? PatchFlags.FULL_PROPS ? PatchFlags.FULL_PROPS
: patchFlag | PatchFlags.FULL_PROPS : patchFlag | PatchFlags.FULL_PROPS
: patchFlag, : patchFlag,
@ -772,7 +772,7 @@ export function normalizeVNode(child: VNodeChild): VNode {
// optimized normalization for template-compiled render fns // optimized normalization for template-compiled render fns
export function cloneIfMounted(child: VNode): VNode { export function cloneIfMounted(child: VNode): VNode {
return (child.el === null && child.patchFlag !== PatchFlags.HOISTED) || return (child.el === null && child.patchFlag !== PatchFlags.CACHED) ||
child.memo child.memo
? child ? child
: cloneVNode(child) : cloneVNode(child)

View File

@ -109,10 +109,10 @@ export enum PatchFlags {
*/ */
/** /**
* Indicates a hoisted static vnode. This is a hint for hydration to skip * Indicates a cached static vnode. This is also a hint for hydration to skip
* the entire sub tree since static content never needs to be updated. * the entire sub tree since static content never needs to be updated.
*/ */
HOISTED = -1, CACHED = -1,
/** /**
* A special flag that indicates that the diffing algorithm should bail out * A special flag that indicates that the diffing algorithm should bail out
* of optimized mode. For example, on block fragments created by renderSlot() * of optimized mode. For example, on block fragments created by renderSlot()
@ -139,6 +139,6 @@ export const PatchFlagNames: Record<PatchFlags, string> = {
[PatchFlags.NEED_PATCH]: `NEED_PATCH`, [PatchFlags.NEED_PATCH]: `NEED_PATCH`,
[PatchFlags.DYNAMIC_SLOTS]: `DYNAMIC_SLOTS`, [PatchFlags.DYNAMIC_SLOTS]: `DYNAMIC_SLOTS`,
[PatchFlags.DEV_ROOT_FRAGMENT]: `DEV_ROOT_FRAGMENT`, [PatchFlags.DEV_ROOT_FRAGMENT]: `DEV_ROOT_FRAGMENT`,
[PatchFlags.HOISTED]: `HOISTED`, [PatchFlags.CACHED]: `HOISTED`,
[PatchFlags.BAIL]: `BAIL`, [PatchFlags.BAIL]: `BAIL`,
} }