共计 8841 个字符,预计需要花费 23 分钟才能阅读完成。
前言
动态节点晋升 是「Vue3」针对 VNode
更新过程性能问题而提出的一个优化点。家喻户晓,在大型利用场景下,「Vue2.x」的 patchVNode
过程,即 diff
过程是十分迟缓的,这是一个非常令人头疼的问题。
尽管,对于面试常问的 diff
过程在肯定水平上是缩小了对 DOM
的间接操作。然而,这个缩小是有肯定老本的。因为,如果是简单利用,那么就会存在父子关系非常复杂的 VNode
,而这也就是 diff
的痛点,它会一直地递归调用 patchVNode
,一直重叠而成的几毫秒,最终就会造成 VNode
更新迟缓。
也因而,这也是为什么咱们所看到的大型利用诸如阿里云之类的采纳的是基于「React」的技术栈的起因之一。所以,「Vue3」也是改过自新,重写了整个 Compiler
过程,提出了动态晋升、靶向更新等优化点,来进步 patchVNode
过程。
那么,回到明天的正题,咱们从源码角度看看在整个编译过程「Vue3」动态节点晋升到底是 何许人也?
什么是 patchFlag
因为,在 compile
过程的 transfrom
阶段会提及 AST Element 上的 patchFlag
属性。所以,在正式意识 complie
之前,咱们先搞清楚一个概念,什么是 patchFlag
?
patchFlag
是 complier
时的 transform
阶段解析 AST Element 打上的 优化标识。并且,顾名思义 patchFlag
,patch
一词示意着它会为 runtime
时的 patchVNode
提供根据,从而实现靶向更新 VNode
的成果。因而,这样一来一往,也就是耳熟能详的 Vue3 奇妙联合 runtime
与 compiler
实现靶向更新和动态晋升。
而在源码中 patchFlag
被定义为一个 数字枚举类型,每一个枚举值对应的标识意义会是这样:
并且,值得一提的是整体上 patchFlag
的分为两大类:
- 当
patchFlag
的值 大于 0 时,代表所对应的元素在patchVNode
时或render
时是能够被优化生成或更新的。 - 当
patchFlag
的值 小于 0 时,代表所对应的元素在patchVNode
时,是须要被full diff
,即进行递归遍历VNode tree
的比拟更新过程。
其实,还有两类非凡的
flag
:shapeFlag
和shapeFlag
,这里我就不对此开展,有趣味的同学能够自行去理解。
Compile 编译过程
比照 Vue2.x 编译过程
理解过「Vue2.x」源码的同学,我想应该都晓得在「Vue2.x」中的 Compile
过程会是这样:
parse
编译模板生成原始 AST。optimize
优化原始 AST,标记 AST Element 为动态根节点或动态节点。generate
依据优化后的 AST,生成可执行代码,例如_c
、_l
之类的。
而在「Vue3」中,整体的 Compile
过程依然是三个阶段,然而不同于「Vue2.x」的是,第二个阶段换成了失常编译器都会存在的阶段 transform
。所以,它看起来会是这样:
在源码中,它对应的伪代码会是这样:
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})
)
}
那么,我想这个时候大家可能会问为什么会是 transform
?它的职责是什么?
通过简略的比照「Vue2.x」编译过程的第二阶段的 optimize
,很显著,transform
并不是 无米之炊 ,它依然有着 优化 原始 AST 的作用,而具体职责会体现在:
- 对所有 AST Element 新增
codegen
属性来帮忙generate
更精确地生成 最优 的可执行代码。 - 对动态 AST Element 新增
hoists
属性来实现动态节点的 独自创立。 - …
此外,transform
还标识了诸如 isBlock
、helpers
等属性,来生成最优的可执行代码,这里咱们就不细谈,有趣味的同学能够自行理解。
baseParse 构建原始形象语法树(AST)
baseParse
顾名思义起着 解析 的作用,它的体现和「Vue2.x」的 parse
雷同,都是解析模板 tempalte
生成 原始 AST。
假如,此时咱们有一个这样的模板 template
:
<div><div>hi vue3</div><div>{{msg}}</div></div>
那么,它在通过 baseParse
解决后生成的 AST 看起来会是这样:
{
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
}
如果,理解过「Vue2.x」编译过程的同学应该对于下面这颗 AST
的大部分属性不会生疏。AST
的实质是通过用对象来形容「DSL」(非凡畛域语言),例如:
children
中寄存的就是最外层div
的后辈。loc
则用来形容这个 AST Element 在整个字符串(template
)中的地位信息。type
则是用于形容这个元素的类型(例如 5 为插值、2 为文本)等等。
并且,能够看到的是不同于「Vue2.x」的 AST,这里咱们多了诸如 helpers
、codegenNode
、hoists
等属性。而,这些属性会在 transform
阶段进行相应地赋值,进而帮忙 generate
阶段生成 更优的 可执行代码。
transfrom 优化原始形象语法树(AST)
对于 transform
阶段,如果理解过 编译器 的工作流程的同学应该晓得,一个残缺的编译器的工作流程会是这样:
- 首先,
parse
解析原始代码字符串,生成形象语法树 AST。 - 其次,
transform
转化形象语法树,让它变成更贴近指标「DSL」的构造。 - 最初,
codegen
依据转化后的形象语法树生成指标「DSL」的可执行代码。
而在「Vue3」采纳 Monorepo
的形式治理我的项目后,compile
对应的能力就是一个编译器。所以,transform
也是整个编译过程的重中之重。换句话说,如果没有 transform
对 AST 做诸多层面的转化,「Vue」依然会挂在 diff
这个 饱受诟病 的过程。
相比之下,「Vue2.x」的编译阶段没有残缺的
transform
,只是optimize
优化了一下 AST,能够设想在「Vue」设计之初尤大也没想到它当前会 这么地风行!
那么,咱们来看看 transform
函数源码中的定义:
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
}
能够说,transform
函数做了什么,在它的定义中是 和盘托出。这里咱们提一下它对动态晋升其决定性作用的两件事:
- 将原始 AST 中的动态节点对应的 AST Element 赋值给根 AST 的
hoists
属性。 - 获取原始 AST 须要的 helpers 对应的键名,用于
generate
阶段的生成可执行代码的获取对应函数,例如createTextVNode
、createStaticVNode
、renderList
等等。
并且,在 traverseNode
函数中会对 AST Element 利用具体的 transform
函数,大抵能够分为两类:
- 动态节点
transform
利用,即节点不含有插值、指令、props、动静款式的绑定等。 - 动静节点
transform
利用,即节点含有插值、指令、props、动静款式的绑定等。
那么,咱们就来看看对于动态节点 transform
是如何利用的?
动态节点 transform
利用
这里,对于下面咱们说到的这个栗子,动态节点就是这个局部:
<div>hi vue3</div>
而它在没有进行 transform
利用之前,它对应的 AST 会是这样:
{
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
}
能够看出,此时它的 codegenNode
是 undefined
。而在源码中各类 transform
函数被定义为 plugin
,它会依据 baseParse
生成的 AST 递归利用 对应的 plugin
。而后,创立对应 AST Element 的 codegen
对象。
所以,此时咱们会命中 transformElement
和 transformText
两个 plugin
的逻辑。
transformText
transformText
顾名思义,它和 文本 相干。很显然,此时的 AST Element 所属的类型就是 Text
。那么,咱们先来看一下 transformText
函数对应的伪代码:
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
}
...
}
}
}
能够看到,这里咱们会命中 {2} 的逻辑,即如果对于 节点含有繁多文本 transformText
并不需要进行额定的解决,即该节点依然在这里依然保留和「Vue2.x」版本一样的解决形式。
而 transfromText
真正发挥作用的场景是当模板中存在这样的状况:
<div>ab {a} {b}</div>
此时 transformText
须要将两者放在一个 独自的 AST Element 下,在源码中它被称为「Compound Expression」,即 组合的表达式 。这种组合的目标是为了 patchVNode
这类 VNode
时做到 更好地定位和实现 DOM
的更新。反之,如果是一个文本节点和插值动静节点的话,在 patchVNode
阶段同样的操作须要进行两次,例如对于同一个 DOM
节点操作两次。
transformElement
transformElement
是一个所有 AST Element 都会被执行的一个 plugin
,它的外围是为 AST Element 生成最根底的 codegen
属性。例如标识出对应 patchFlag
,从而为生成 VNode
提供根据,例如 dynamicChildren
。
而对于动态节点,同样是起到一个初始化它的 codegenNode
属性的作用。并且,从下面介绍的 patchFlag
的类型,咱们能够晓得它的 patchFlag
为默认值 0
。所以,它的 codegenNode
属性值看起来会是这样:
{
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 生成可执行代码
generate
是 compile
阶段的最初一步,它的作用是将 transform
转换后的 AST 生成对应的 可执行代码,从而在之后 Runtime 的 Render 阶段时,就能够通过可执行代码生成对应的 VNode Tree,而后最终映射为实在的 DOM Tree 在页面上。
同样地,这一阶段在「Vue2.x」也是由 generate
函数实现,它会生成是诸如 _l
、_c
之类的函数,这实质上是对 _createElement
函数的封装。而相比拟「Vue2.x」版本的 generate
,「Vue3」扭转了很多,其 generate
函数对于的伪代码会是这样:
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}
}
所以,接下来,咱们就来 一睹 带有动态节点对应的 AST 生成的可执行代码的过程会是怎么。
CodegenContext 代码生成上下文
从下面 generate
函数的伪代码能够看到,在函数的开始调用了 createCodegenContext
为以后 AST 生成了一个 context
。在整个 generate
函数的执行过程 都依靠 于一个 CodegenContext
生成代码上下文(对象)的能力,它是通过 createCodegenContext
函数生成。而 CodegenContext
的接口定义会是这样:
interface CodegenContext
extends Omit<Required<CodegenOptions>, 'bindingMetadata'> {
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}
能够看到 CodegenContext
对象中有诸如 push
、indent
、newline
之类的办法。而它们的作用是在依据 AST 来生成代码时用来 实现换行 、 增加代码 、 缩进 等性能。从而,最终造成一个个可执行代码,即咱们所认知的 render
函数,并且,它会作为 CodegenContext
的 code
属性的值返回。
上面,咱们就来看下动态节点的可执行代码生成的外围,它被称为 Preamble
前导。
genFunctionPreamble 生成前筹备
整个动态晋升的可执行代码生成就是在 genFunctionPreamble
函数局部实现的。并且,大家认真 斟酌 一番动态晋升的字眼,动态二字咱们能够不看,然而 晋升二字 ,直抒本意地表白出它(动态节点)被 进步了。
为什么说是进步了?因为在源码中的体现,的确是被进步了。在后面的 generate
函数,咱们能够看到 genFunctionPreamble
是先于 render
函数退出 context.code
中,所以,在 Runtime 阶段的 Render,它会先于 render
函数执行。
geneFunctionPreamble
函数(伪代码):
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 `)
}
能够看到,这里会对后面咱们在 transform
函数提及的 hoists
属性的长度进行判断。显然,对于后面说的这个栗子,它的 ast.hoists.length
长度是大于 0 的。所以,这里就会依据 hoists
中的 AST 生成对应的可执行代码。因而,到这里,生成的可执行代码会是这样:
const _Vue = Vue
const {createVNode: _createVNode} = _Vue
// 动态晋升局部
const _hoisted_1 = _createVNode("div", null, "hi vue3", -1 /* HOISTED */)
// render 函数会在这上面
小结
动态节点晋升在整个 compile
编译阶段体现,从最后的 baseCompile
到 transform
转化原始 AST、再到 generate
的优先 render
函数解决生成可执行代码,最初交给 Runtime 时的 Render 执行,这种设计能够说是十分精妙!所以,这样一来,就实现了咱们常常看到在一些文章提及的「Vue3」对于动态节点在整个生命周期中它只会执行 一次创立 的源码实现,这在肯定水平上升高了性能上的开销。
写在最初
看完动态的节点在整个编译过程的解决,我想大家可能都急不可待地想去理解对于动态节点的 patchVNode
又是 怎么一番现象?原先,我是打算在一篇文章形容残缺个过程,然而起初思考,这无形中给浏览减少了老本。因为,在「Vue3」版本的 patchVNode
已不仅仅是 diff
的比拟过程,它对于每一种 VNode
都实现了不同的 patch
过程。所以,patchVNode
的过程会在写在下一篇文章,敬请期待!
往期文章回顾
从零到一,带你彻底搞懂 vite 中的 HMR 原理(源码剖析)
详解,从后端导出文件到前端(Blob)下载过程
❤️ 爱心三连击
通过浏览,如果你感觉有播种的话,能够爱心三连击!!!