问题引入:为什么说Vue3的渲染速度优于Vue2
问题解释
想搞清楚这个问题, 我们就需要了解Vue2中是如何生成VNode的.
Vue2中如何生成VNode树
在 Vue2 中,VNode 的生成主要通过调用 createElement
方法实现,这个方法在运行时的 compiler
模块中被定义。在编译模版时,模版会被转换为 render 函数,而 render 函数在执行时会调用 createElement
方法生成 VNode。下面是 createElement
方法的源码实现:
源码解析
export function createElement(
context: Component,
tag: any,
data: any,
children: any,
normalizationType: any,
alwaysNormalize: boolean
): VNode | Array<VNode> {
if (isArray(data) || isPrimitive(data)) {
normalizationType = children
children = data
data = undefined
}
if (isTrue(alwaysNormalize)) {
normalizationType = ALWAYS_NORMALIZE
}
return _createElement(context, tag, data, children, normalizationType)
}
export function _createElement (
context: Component,
tag?: string | Component | Function | Object,
data?: VNodeData,
children?: any,
normalizationType?: number
): : VNode | Array<VNode> {
// 省略一些非关键代码
// 如果tag是字符串,则视为普通元素,创建对应的VNode对象
if (typeof tag === 'string') {
let Ctor
ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
if (config.isReservedTag(tag)) {
// 如果是平台保留标签,直接创建对应的VNode对象
vnode = new VNode(
config.parsePlatformTagName(tag),
data,
children,
undefined,
undefined,
context
)
} else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
// 如果tag是组件名,则根据组件定义创建对应的VNode对象
vnode = createComponent(Ctor, data, context, children, tag)
} else {
// 否则创建未知标签对应的VNode对象
vnode = new VNode(
tag, data, children,
undefined, undefined, context
)
}
} else {
// 如果tag不是字符串,则视为组件选项对象,创建对应的VNode对象
vnode = createComponent(tag, data, context, children)
}
// 省略一些非关键代码
// 返回生成的VNode对象
return vnode
}
createElement
方法接收五个参数:
context
: 当前组件实例tag
: VNode 节点的标签名data
: VNode 节点的数据,例如节点的属性和事件等children
: VNode 节点的子节点normalizationType
: 是否需要对子节点进行标准化alwaysNormalize
: 是否总是对子节点进行标准化
通过调用 createElement
方法,我们可以创建一个 VNode 对象,VNode 对象用于表示一个虚拟的节点,包含了该节点的标签名、属性、子节点等信息。
从上面的代码可以看出,在Vue2中,createElement函数内部并不直接使用AST,而是根据传入的参数动态创建VNode对象。创建DOM需要遍历VNode树,通过递归调用createElement函数实现的
Vue3中如何生成VNode树
在 Vue3 中,生成 VNode 树的过程是通过解析模板文件得到 AST,再通过 AST 生成 VNode 树。这一过程主要是由 compiler-core
模块中的 baseCompile
函数完成的。
源码解析
// packages/compiler-core/src/compile.ts
import { baseParse } from './parse'
import { transform } from './transform'
import { generate } from './codegen'
import { RootNode, NodeTypes } from './ast'
import { CompilerOptions } from './options'
export function baseCompile(
template: string | RootNode,
options: CompilerOptions = {}
): CodegenResult {
// 1. parse 阶段,将模板转换为 AST
const ast = isString(template) ? baseParse(template, options) : template
// 2. transform 阶段,对 AST 进行静态分析和优化
transform(
ast,
extend({}, options, {
prefixIdentifiers,
nodeTransforms: [ // 用于处理模板中的节点
...nodeTransforms,
...(options.nodeTransforms || []) // user transforms
],
directiveTransforms: extend( // 用于处理模板中的指令
{},
directiveTransforms,
options.directiveTransforms || {} // user transforms
)
})
)
// 3. generate 阶段,将 AST 转换为可执行的代码
return generate(
ast,
extend({}, options, {
prefixIdentifiers
})
)
}
Vue3中, 废弃了createElement
方法,使用compile
方法对template进行编译,虽然在生成render函数前多了AST的解析的步骤,但通过这一步骤完善了VNode的各项属性,使得无需遍历VNode树即可生成DOM元素。
性能对比
我们可以设计一个相同结构的template用于验证Vue2和Vue3的性能差异, 测试代码如下:
// Vue2
const vm = new Vue({
el: '#app',
render(h) {
const vNodes = []
for (let i = 0; i < 10000; i++) {
vNodes.push(h('div', {
attrs: {
class: 'wrapper'
}
}, [
h('h1', 'Hello, world!'),
h('p', 'This is a performance test.')
]))
}
return h('div', vNodes)
}
})
// Vue3
const { createApp, createVNode } = Vue
const app = createApp({
render() {
const template = createVNode('div', {
class: 'wrapper'
}, [
createVNode('h1', null, 'Hello, world!'),
createVNode('p', null, 'This is a performance test.')
])
const elements = []
for (let i = 0; i < 10000; i++) {
elements.push(template)
}
return createVNode('div', null, elements)
}
})
app.mount('#app')
下面是Chrome Devtools – Performance截图
Vue2
Vue3
测试结果显示,使用Vue3中的AST生成VNode性能更优。
总结
通过重构的解析器,Vue3在处理复杂、庞大的template时,可以更加快速的生成、渲染DOM。
参考链接
原文链接:https://juejin.cn/post/7216965516878561336 作者:没空铲屎的艾伦