May 31, 2021 Article blog
The article comes from the public number: Code Center, author Wu Liu
Static node promotion
is an optimization point proposed by Vue3 for performance issues in
VNode
update process.
It is well known that in large application scenarios, the
patchVNode
process of
"Vue2.x",
or
diff
is very slow, which is a very troubling problem.
Although, the
diff
process, which is often asked for interviews, is partly a reduction in direct operation on the
DOM
H
owever,
"this reduction has a certain cost".
Because, if it's a complex application, there's a very complex
VNode
with a very complex parent-child relationship, and that's the pain point of
diff
which constantly
patchVNode
recursively and stacks for milliseconds, eventually causing
VNode
to update slowly.
So that's one of the reasons why we're seeing large applications like Alibaba Cloud adopt a "React"-based technology stack.
Therefore, "Vue3" is also a painful change before, rewriting the entire
Compiler
process, proposed static lifting, targeted updates and other optimization points to improve
patchVNode
process.
So, back to today's question, from a source point of view, the whole compilation process "Vue3" static node promotion is "what kind of people also"?
Because the
patchFlag
property on AST Element is mentioned during the
transfrom
phase of the
compile
process.
So, before we get to know
complie
let's figure out a concept, what is
patchFlag
patchFlag
is the
"optimized identity"
on the
transform
phase resolution AST Element at the time of
complier
A
lso, as the name implies,
patchFlag
the word
patch
indicates that it provides a basis for
patchVNode
at
runtime
for targeted updates to
VNode
As a result, the familiar Vue3 cleverly combines
runtime
with
compiler
for targeted updates and static lifts.
In the source
patchFlag
is defined as a
"digital enumeration type"
and the identity meaning for each enumerated value is as follows:
Also, it's worth noting that
patchFlag
as a whole falls into two broad categories:
patchFlag
is "greater than"
0, the corresponding element can be optimally generated or updated at
patchVNode
or
render
patchFlag
is "less than"
0, representing the corresponding element in
patchVNode
it is required to be
full diff
which is a recursive traversal of
VNode tree
comparative update process.
In fact, there are two special types of
flag
shapeFlag
andslogFlag
which I do not expand here, interested students can learn about themselves.
Having learned about
the "Vue2.x"
source code, I think I should know that the
Compile
process in
"Vue2.x"
would look like this:
parse
compilation template generates the original AST.
optimize
optimizes the original AST, marking AST Element as either a static root node or a static node.
generate
generates executable code, such as
_c
based on the optimized AST.
_l
In Vue3, the overall
Compile
process is still three phases, but unlike Vue2.x, the second stage is replaced by the stage
transform
that the normal compiler will have.
So, it looks like this:
In the source code, the pseudocode it corresponds to is this:
export function baseCompile(
template: string | RootNode,
options: CompilerOptions = {}
): CodegenResult {
...
const ast = isString(template) ? baseParse(template, options) : template
...
transform(
ast,
extend({}, options, {....})
)
return generate(
ast,
extend({}, options, {
prefixIdentifiers
})
)
}
So, I think at this point people might ask why
transform
What is its responsibility?
By simply comparing
optimize
of the second phase of the
"Vue2.x"
compilation process, it is clear that
transform
is not
a "riceless cooking"
and still has the role of
"optimizing"
the original AST, and that the specific responsibilities are:
codegen
properties to all AST Element to help
generate
"optimal"
executable code more accurately.
hoists
properties to static AST Element to implement
"separate creation"
of static nodes.
In addition,
transform
identifies properties such as
isBlock
helpers
etc. to generate optimal executable code, which we won't discuss in detail, and interested students can learn about it themselves.
baseParse
as the name implies, acts as
"parsing"
and behaves the same as
parse
of
"Vue2.x",
which is the parsing template
tempalte
that generates
the "original AST".
Suppose, at this point we have a
template
template like this:
<div><div>hi vue3</div><div>{{msg}}</div></div>
Well, the AST it generates after
baseParse
processing looks like this:
{
cached: 0,
children: [{…}],
codegenNode: undefined,
components: [],
directives: [],
helpers: [],
hoists: [],
imports: [],
loc: {start: {…}, end: {…}, source: "<div><div>hi vue3</div><div>{{msg}}</div></div>"},
temps: 0,
type: 0
}
If, students who have learned about
the "Vue2.x"
compilation process should be no stranger to most of the properties of the
AST
above.
The essence of
AST
is to describe "DSL" (special domain language) by using objects, for example:
children
is the descendants of the outermost
div
loc
is used to describe the location of this AST Element throughout the string
template
type
is used to describe the type of element (e.g. 5 is interpolation, 2 is text), and so on.
Also, what you can see is a different AST from "Vue2.x", where we have properties such as
helpers
codegenNode
hoists
and so on.
Instead, these properties are assigned accordingly in the
transform
phase, which in turn
generate
the generate phase generate
"better"
executable code.
For
transform
phase, students who have known the
compiler
workflow should know that a complete compiler workflow will look like this:
parse
parses the original code string to generate an abstract syntax tree AST.
transform
transforms the abstract syntax tree into a structure closer to the target "DSL".
codegen
executable code for the target "DSL" based on the converted abstract syntax tree.
After Vue3 manages the project in
Monorepo
the corresponding capability of
compile
is a compiler. T
herefore,
transform
is also a top priority throughout the compilation process.
In other words, "Vue" would still hang on to
diff
a
"much-maligned"
process, without
transform
the AST
on many levels.
In contrast, the compilation phase of "Vue2.x" did not have a complete
transform
butoptimize
optimized the AST, you can imagine at the beginning of the "Vue" design was particularly large did not expect it will be "so popular" in the future!
So, let's look at the definition in the
transform
function source code:
function transform(root: RootNode, options: TransformOptions) {
const context = createTransformContext(root, options)
traverseNode(root, context)
if (options.hoistStatic) {
hoistStatic(root, context)
}
if (!options.ssr) {
createRootCodegen(root, context)
}
// finalize meta information
root.helpers = [...context.helpers]
root.components = [...context.components]
root.directives = [...context.directives]
root.imports = [...context.imports]
root.hoists = context.hoists
root.temps = context.temps
root.cached = context.cached
}
It can be said that what the
transform
function does is defined as
"unobstructed"
in its definition.
Here we mention two things that make it decisive for static ascension:
hoists
property of the root AST.
generate
phase, such as
createTextVNode
createStaticVNode
renderList
and so on.
Also, in the
traverseNode
function, a specific
transform
function is applied to AST Element, which can be broadly divided into two categories:
transform
application, i.e. node does not contain interpolation, instructions, props, dynamic style bindings, etc.
transform
app, i.e. node contains interpolation, instructions, props, dynamic style bindings, and so on.
So, let's see how
transform
is applied to static nodes.
transform
app
Here, for the chestnut we mentioned above, the static node is this part:
<div>hi vue3</div>
And before it's applied to
transform
its corresponding AST looks like this:
{
children: [{
content: "hi vue3"
loc: {start: {…}, end: {…}, source: "hi vue3"}
type: 2
}],
codegenNode: undefined,
isSelfClosing: false,
loc: {start: {…}, end: {…}, source: "<div>hi vue3</div>"},
ns: 0,
props: [],
tag: "div",
tagType: 0,
type: 1
}
As you can see, at this point its
codegenNode
is
undefined
I
n the source code, the various
transform
functions are defined as
plugin
which corresponds to the AST
"recursive app"
generated
plugin
baseParse
Then, create the
codegen
object for AST Element.
So, at this point we'll hit the logic
plugin
transformElement
and
transformText
「transformText」
transformText
as the name suggests, is related
to "text".
O
bviously, the type of AST Element belongs to at this point is
Text
So, let's take a look at the pseudocode for the
transformText
function:
export const transformText: NodeTransform = (node, context) => {
if (
node.type === NodeTypes.ROOT ||
node.type === NodeTypes.ELEMENT ||
node.type === NodeTypes.FOR ||
node.type === NodeTypes.IF_BRANCH
) {
return () => {
const children = node.children
let currentContainer: CompoundExpressionNode | undefined = undefined
let hasText = false
for (let i = 0; i < children.length; i++) { // {1}
const child = children[i]
if (isText(child)) {
hasText = true
...
}
}
if (
!hasText ||
(children.length === 1 &&
(node.type === NodeTypes.ROOT ||
(node.type === NodeTypes.ELEMENT &&
node.tagType === ElementTypes.ELEMENT)))
) { // {2}
return
}
...
}
}
}
As you can see, here we hit the logic of
"{2}",
that is, if there is no need for additional processing for
"nodes containing single text"
transformText
that is, the node still retains the same treatment here as the "Vue2.x" version.
The scenario in which
transfromText
really works is when there is a situation in the template:
<div>ab {a} {b}</div>
At this
transformText
needs to place both under a
"separate"
AST Element, which in the source code is called "Compound Expression," or
"combined expression."
T
he purpose of this combination is to
"better position and implement
DOM
updates"
when VNode such as
patchVNode
VNode
Conversely, if it is a text node and interpolation dynamic node, the same operation in the
patchVNode
phase requires two operations, such as twice for the same
DOM
node.
「transformElement」
transformElement
is a
plugin
that is executed by all AST Element, and at its core is the generation of the most basic
codegen
properties for AST Element.
For example, identify the corresponding
patchFlag
to provide a basis for generating
VNode
such as
dynamicChildren
For static nodes, it also acts as an initialization of its
codegenNode
property. A
nd, from the type of
patchFlag
described above, we can know that its
patchFlag
is the default value of
0
So, its
codegenNode
property value looks like this:
{
children: {
content: "hi vue3"
loc: {start: {…}, end: {…}, source: "hi vue3"}
type: 2
},
directives: undefined,
disableTracking: false,
dynamicProps: undefined,
isBlock: false,
loc: {start: {…}, end: {…}, source: "<div>hi vue3</div>"},
patchFlag: undefined,
props: undefined,
tag: ""div"",
type: 13
}
generate
is the final step in the
compile
phase, which is to generate the corresponding
"executable code"
for the
transform
AST so that, at the Render stage of Runtime, the corresponding VNode Tree can be generated from executable code and then eventually mapped to the real DOM Tree on the page.
Similarly, this phase in "Vue2.x" is done by the
generate
function, which generates functions such as
_l
_c
which essentially encapsulates
_createElement
functions.
Compared to the "Vue2.x" version of
generate
"Vue3" has changed a lot, and the pseudocode corresponding to its
generate
function will look like this:
export function generate(
ast: RootNode,
options: CodegenOptions & {
onContextCreated?: (context: CodegenContext) => void
} = {}
): CodegenResult {
const context = createCodegenContext(ast, options)
if (options.onContextCreated) options.onContextCreated(context)
const {
mode,
push,
prefixIdentifiers,
indent,
deindent,
newline,
scopeId,
ssr
} = context
...
genFunctionPreamble(ast, context)
...
if (!ssr) {
...
push(`function render(_ctx, _cache${optimizeSources}) {`)
}
....
return {
ast,
code: context.code,
// SourceMapGenerator does have toJSON() method but it's not in the types
map: context.map ? (context.map as any).toJSON() : undefined
}
}
So, next, let's "see" what the process of executable code generated with a static node corresponds to the AST.
As you can see from the pseudocode of the
generate
function above,
createCodegenContext
was called at the beginning of the function to generate a context for
context
current AST. T
he entire execution of the
generate
function
"relies" on
the ability of a
CodegenContext
to "generate code contexts"
(objects), which is generated through
createCodegenContext
function.
CodegenContext
interface definition looks like this:
interface CodegenContext
extends Omit {
source: string
code: string
line: number
column: number
offset: number
indentLevel: number
pure: boolean
map?: SourceMapGenerator
helper(key: symbol): string
push(code: string, node?: CodegenNode): void
indent(): void
deindent(withoutNewLine?: boolean): void
newline(): void
}
You can see methods such as
push
indent
newline
etc. in
CodegenContext
objects. T
heir role is to
"implement line breaks," "add code,"
"indentation"
and other features when
generating
code based on AST.
Thus, an executable code is eventually formed, the
render
function as we know it, and it is returned as the value
code
code properties of
CodegenContext
Let's look at the core of executable code generation for static nodes, known as
Preamble
leading.
The entire statically promoted executable code generation is done in the
genFunctionPreamble
function section.
Moreover, we carefully
"consider"
the word static ascension, static two words we can not read, but
"ascending two words",
directly express it (static node) is
"improved".
Why is it improved? B
ecause the embodiment in the source code, is indeed improved.
In the previous
generate
function, we can see that
genFunctionPreamble
joins
context.code
before
render
function, so in the Render stage at Runtime, it
render
before the render function.
geneFunctionPreamble
function (pseudocode):
function genFunctionPreamble(ast: RootNode, context: CodegenContext) {
const {
ssr,
prefixIdentifiers,
push,
newline,
runtimeModuleName,
runtimeGlobalName
} = context
...
const aliasHelper = (s: symbol) => `${helperNameMap[s]}: _${helperNameMap[s]}`
if (ast.helpers.length > 0) {
...
if (ast.hoists.length) {
const staticHelpers = [
CREATE_VNODE,
CREATE_COMMENT,
CREATE_TEXT,
CREATE_STATIC
]
.filter(helper => ast.helpers.includes(helper))
.map(aliasHelper)
.join(', ')
push(`const { ${staticHelpers} } = _Vue\n`)
}
}
...
genHoists(ast.hoists, context)
newline()
push(`return `)
}
As you can see, the length of the
hoists
property mentioned earlier in the
transform
function is judged here. O
bviously, for this chestnut mentioned earlier, its
ast.hoists.length
is greater than 0. T
herefore, the corresponding executable code is generated based on the AST in
hoists
So here, the resulting executable code looks like this:
const _Vue = Vue
const { createVNode: _createVNode } = _Vue
// 静态提升部分
const _hoisted_1 = _createVNode("div", null, "hi vue3", -1 /* HOISTED */)
// render 函数会在这下面
Static node promotion is reflected throughout
compile
compilation phase, from the initial
baseCompile
to
transform
conversion of the original AST, to the generated executable code by
generate
priority
render
function, which is finally delivered to The Render at Runtime, which is a very subtle design!
So, this completes the source code implementation that we often see in some articles that "Vue3" performs only
"one creation"
for static nodes throughout its lifecycle, which reduces performance overhead to some extent.
That's what
W3Cschool编程狮
has to say
about Vue 3.0 diff's new feature, Static Node Promotion,
and I hope it will help you.