Vue3 正式发布已经有一段时间了,前段时间写了一篇文章(《Vue 模板编译原理》)分析 Vue 的模板编译原理。今天的文章打算学习下 Vue3 下的模板编译与 Vue2 下的差异,以及 VDOM 下 Diff 算法的优化。

公司主营业务:成都做网站、网站制作、移动网站开发等业务。帮助企业客户真正实现互联网宣传,提高企业的竞争能力。成都创新互联是一支青春激扬、勤奋敬业、活力青春激扬、勤奋敬业、活力澎湃、和谐高效的团队。公司秉承以“开放、自由、严谨、自律”为核心的企业文化,感谢他们对我们的高要求,感谢他们从不同领域给我们带来的挑战,让我们激情的团队有机会用头脑与智慧不断的给客户带来惊喜。成都创新互联推出余干免费做网站回馈大家。
编译入口
了解过 Vue3 的同学肯定知道 Vue3 引入了新的组合 Api,在组件 mount 阶段会调用 setup 方法,之后会判断 render 方法是否存在,如果不存在会调用 compile 方法将 template 转化为 render。
- // packages/runtime-core/src/renderer.ts
 - const mountComponent = (initialVNode, container) => {
 - const instance = (
 - initialVNode.component = createComponentInstance(
 - // ...params
 - )
 - )
 - // 调用 setup
 - setupComponent(instance)
 - }
 - // packages/runtime-core/src/component.ts
 - let compile
 - export function registerRuntimeCompiler(_compile) {
 - compile = _compile
 - }
 - export function setupComponent(instance) {
 - const Component = instance.type
 - const { setup } = Component
 - if (setup) {
 - // ...调用 setup
 - }
 - if (compile && Component.template && !Component.render) {
 - // 如果没有 render 方法
 - // 调用 compile 将 template 转为 render 方法
 - Component.render = compile(Component.template, {...})
 - }
 - }
 
这部分都是 runtime-core 中的代码,之前的文章有讲过 Vue 分为完整版和 runtime 版本。如果使用 vue-loader 处理 .vue 文件,一般都会将 .vue 文件中的 template 直接处理成 render 方法。
- // 需要编译器
 - Vue.createApp({
 - template: '
 {{ hi }}'- })
 - // 不需要
 - Vue.createApp({
 - render() {
 - return Vue.h('div', {}, this.hi)
 - }
 - })
 
完整版与 runtime 版的差异就是,完整版会引入 compile 方法,如果是 vue-cli 生成的项目就会抹去这部分代码,将 compile 过程都放到打包的阶段,以此优化性能。runtime-dom 中提供了 registerRuntimeCompiler 方法用于注入 compile 方法。
主流程
在完整版的 index.js 中,调用了 registerRuntimeCompiler 将 compile 进行注入,接下来我们看看注入的 compile 方法主要做了什么。
- // packages/vue/src/index.ts
 - import { compile } from '@vue/compiler-dom'
 - // 编译缓存
 - const compileCache = Object.create(null)
 - // 注入 compile 方法
 - function compileToFunction(
 - // 模板
 - template: string | HTMLElement,
 - // 编译配置
 - options?: CompilerOptions
 - ): RenderFunction {
 - if (!isString(template)) {
 - // 如果 template 不是字符串
 - // 则认为是一个 DOM 节点,获取 innerHTML
 - if (template.nodeType) {
 - template = template.innerHTML
 - } else {
 - return NOOP
 - }
 - }
 - // 如果缓存中存在,直接从缓存中获取
 - const key = template
 - const cached = compileCache[key]
 - if (cached) {
 - return cached
 - }
 - // 如果是 ID 选择器,这获取 DOM 元素后,取 innerHTML
 - if (template[0] === '#') {
 - const el = document.querySelector(template)
 - template = el ? el.innerHTML : ''
 - }
 - // 调用 compile 获取 render code
 - const { code } = compile(
 - template,
 - options
 - )
 - // 将 render code 转化为 function
 - const render = new Function(code)();
 - // 返回 render 方法的同时,将其放入缓存
 - return (compileCache[key] = render)
 - }
 - // 注入 compile
 - registerRuntimeCompiler(compileToFunction)
 
在讲 Vue2 模板编译的时候已经讲过,compile 方法主要分为三步,Vue3 的逻辑类似:
- // packages/compiler-dom/src/index.ts
 - import { baseCompile, baseParse } from '@vue/compiler-core'
 - export function compile(template, options) {
 - return baseCompile(template, options)
 - }
 - // packages/compiler-core/src/compile.ts
 - import { baseParse } from './parse'
 - import { transform } from './transform'
 - import { transformIf } from './transforms/vIf'
 - import { transformFor } from './transforms/vFor'
 - import { transformText } from './transforms/transformText'
 - import { transformElement } from './transforms/transformElement'
 - import { transformOn } from './transforms/vOn'
 - import { transformBind } from './transforms/vBind'
 - import { transformModel } from './transforms/vModel'
 - export function baseCompile(template, options) {
 - // 解析 html,转化为 ast
 - const ast = baseParse(template, options)
 - // 优化 ast,标记静态节点
 - transform(ast, {
 - ...options,
 - nodeTransforms: [
 - transformIf,
 - transformFor,
 - transformText,
 - transformElement,
 - // ... 省略了部分 transform
 - ],
 - directiveTransforms: {
 - on: transformOn,
 - bind: transformBind,
 - model: transformModel
 - }
 - })
 - // 将 ast 转化为可执行代码
 - return generate(ast, options)
 - }
 
计算 PatchFlag
这里大致的逻辑与之前的并没有多大的差异,主要是 optimize 方法变成了 transform 方法,而且默认会对一些模板语法进行 transform。这些 transform 就是后续虚拟 DOM 优化的关键,我们先看看 transform 的代码 。
- // packages/compiler-core/src/transform.ts
 - export function transform(root, options) {
 - const context = createTransformContext(root, options)
 - traverseNode(root, context)
 - }
 - export function traverseNode(node, context) {
 - context.currentNode = node
 - const { nodeTransforms } = context
 - const exitFns = []
 - for (let i = 0; i < nodeTransforms.length; i++) {
 - // Transform 会返回一个退出函数,在处理完所有的子节点后再执行
 - const onExit = nodeTransforms[i](node, context)
 - if (onExit) {
 - if (isArray(onExit)) {
 - exitFns.push(...onExit)
 - } else {
 - exitFns.push(onExit)
 - }
 - }
 - }
 - traverseChildren(node, context)
 - context.currentNode = node
 - // 执行所以 Transform 的退出函数
 - let i = exitFns.length
 - while (i--) {
 - exitFns[i]()
 - }
 - }
 
我们重点看一下 transformElement 的逻辑:
- // packages/compiler-core/src/transforms/transformElement.ts
 - export const transformElement: NodeTransform = (node, context) => {
 - // transformElement 没有执行任何逻辑,而是直接返回了一个退出函数
 - // 说明 transformElement 需要等所有的子节点处理完后才执行
 - return function postTransformElement() {
 - const { tag, props } = node
 - let vnodeProps
 - let vnodePatchFlag
 - const vnodeTag = node.tagType === ElementTypes.COMPONENT
 - ? resolveComponentType(node, context)
 - : `"${tag}"`
 - let patchFlag = 0
 - // 检测节点属性
 - if (props.length > 0) {
 - // 检测节点属性的动态部分
 - const propsBuildResult = buildProps(node, context)
 - vnodeProps = propsBuildResult.props
 - patchFlag = propsBuildResult.patchFlag
 - }
 - // 检测子节点
 - if (node.children.length > 0) {
 - if (node.children.length === 1) {
 - const child = node.children[0]
 - // 检测子节点是否为动态文本
 - if (!getStaticType(child)) {
 - patchFlag |= PatchFlags.TEXT
 - }
 - }
 - }
 - // 格式化 patchFlag
 - if (patchFlag !== 0) {
 - vnodePatchFlag = String(patchFlag)
 - }
 - node.codegenNode = createVNodeCall(
 - context,
 - vnodeTag,
 - vnodeProps,
 - vnodeChildren,
 - vnodePatchFlag
 - )
 - }
 - }
 
buildProps 会对节点的属性进行一次遍历,由于内部源码涉及很多其他的细节,这里的代码是经过简化之后的,只保留了 patchFlag 相关的逻辑。
- export function buildProps(
 - node: ElementNode,
 - context: TransformContext,
 - props: ElementNode['props'] = node.props
 - ) {
 - let patchFlag = 0
 - for (let i = 0; i < props.length; i++) {
 - const prop = props[i]
 - const [key, name] = prop.name.split(':')
 - if (key === 'v-bind' || key === '') {
 - if (name === 'class') {
 - // 如果包含 :class 属性,patchFlag | CLASS
 - patchFlag |= PatchFlags.CLASS
 - } else if (name === 'style') {
 - // 如果包含 :style 属性,patchFlag | STYLE
 - patchFlag |= PatchFlags.STYLE
 - }
 - }
 - }
 - return {
 - patchFlag
 - }
 - }
 
上面的代码只展示了三种 patchFlag 的类型:
name: {{name}}
节点包含可变的 style 属性(STYLE = 1 << 2)
可以看到 PatchFlags 都是数字 1 经过 左移操作符 计算得到的。
- export const enum PatchFlags {
 - TEXT = 1, // 1, 二进制 0000 0001
 - CLASS = 1 << 1, // 2, 二进制 0000 0010
 - STYLE = 1 << 2, // 4, 二进制 0000 0100
 - PROPS = 1 << 3, // 8, 二进制 0000 1000
 - ...
 - }
 
从上面的代码能看出来,patchFlag 的初始值为 0,每次对 patchFlag 都是执行 | (或)操作。如果当前节点是一个只有动态文本子节点且同时具有动态 style 属性,最后得到的 patchFlag 为 5(二进制:0000 0101)。
name: {{name}}
我们将上面的代码放到 Vue3 中运行:
- const app = Vue.createApp({
 - data() {
 - return {
 - color: 'red',
 - name: 'shenfq'
 - }
 - },
 - template: `
 `name: {{name}}
- })
 - app.mount('#app')
 
最后生成的 render 方法如下,和我们之前的描述基本一致。
function render() {}
render 优化
Vue3 在虚拟 DOM Diff 时,会取出 patchFlag 和需要进行的 diff 类型进行 &(与)操作,如果结果为 true 才进入对应的 diff。
patchFlag 判断
还是拿之前的模板举例:
name: {{name}}
如果此时的 name 发生了修改,p 节点进入了 diff 阶段,此时会将判断 patchFlag & PatchFlags.TEXT ,这个时候结果为真,表明 p 节点存在文本修改的情况。
patchFlag
- patchFlag = 5
 - patchFlag & PatchFlags.TEXT
 - // 或运算:只有对应的两个二进位都为1时,结果位才为1。
 - // 0000 0101
 - // 0000 0001
 - // ------------
 - // 0000 0001 => 十进制 1
 
- if (patchFlag & PatchFlags.TEXT) {
 - if (oldNode.children !== newNode.children) {
 - // 修改文本
 - hostSetElementText(el, newNode.children)
 - }
 - }
 
但是进行 patchFlag & PatchFlags.CLASS 判断时,由于节点并没有动态 Class,返回值为 0,所以就不会对该节点的 class 属性进行 diff,以此来优化性能。
patchFlag
- patchFlag = 5
 - patchFlag & PatchFlags.CLASS
 - // 或运算:只有对应的两个二进位都为1时,结果位才为1。
 - // 0000 0101
 - // 0000 0010
 - // ------------
 - // 0000 0000 => 十进制 0
 
总结
其实 Vue3 相关的性能优化有很多,这里只单独将 patchFlag 的十分之一的内容拿出来讲了,Vue3 还没正式发布的时候就有看到说 Diff 过程会通过 patchFlag 来进行性能优化,所以打算看看他的优化逻辑,总的来说还是有所收获。
本文转载自微信公众号「更了不起的前端」,可以通过以下二维码关注。转载本文请联系更了不起的前端公众号。