Vue 将模板(template)转换为渲染函数(render function)是 Vue 编译器的核心功能,它是 Vue 实现响应式和虚拟 DOM 的关键步骤。在 Vue 中,模板(template)是开发者编写的类似 HTML 的代码,用于描述页面的结构和交互逻辑。渲染函数(render function)是 Vue 编译器将模板转换为JavaScript 函数,用于生成虚拟 DOM,并最终渲染到页面上。所以render的作用是生成虚拟dom。你可能会想,为什么vue要写template而不是html?为什么写了template通过render将模板生成虚拟dom,而不是直接生成html呢?请往下看
vue模板渲染是个很复杂的过程,牵扯vue的响应式和虚拟dom。本文只抓核心流程,不关心具体实现。纵向深入,不做横向扩展,避免罗里吧嗦又抓不住重点。
vue中的template模板
在Vue中,模板(template)指的是开发者编写的类似HTML的代码,用于描述页面的结构、样式和交互逻辑。模板中可以包含Vue特有的指令、数据绑定、事件处理等内容,通过Vue的编译器将模板转换为渲染函数,最终渲染到页面上。
vue2的template
以下是一个简单的Vue2模板示例:比较简单的Vue模板,描述了页面的结构和交互逻辑,通过Vue的数据绑定和事件处理机制实现了动态更新页面内容的功能
<template><div><h1>{{ message }}</h1><p>计数:{{ count }}</p><button @click="increment">增加</button></div>
</template><script>
export default {data() {return {message: 'Hello, Vue!',count: 0};},methods: {increment() {this.count++;}}
};
</script><style>
h1 {color: blue;
}
p {font-size: 16px;
}
button {background-color: green;color: white;padding: 5px 10px;cursor: pointer;
}
</style>
在上面的示例中,
<template>
标签内包含了页面的结构,包括一个标题、一个段落和一个按钮。模板中使用了双大括号语法{{ }}
进行数据绑定,将message
和count
的值动态显示在页面上。按钮上使用了@click
指令绑定了increment
方法,实现点击按钮增加计数的功能。
vue3的template
以下是一个简单的Vue3模板示例:使用
<script setup>
语法的Vue 3模板,通过更简洁的语法实现了相同的功能。
<template><div><h1>{{ message }}</h1><p>计数:{{ count }}</p><button @click="increment">增加</button></div>
</template><script setup>
import { ref } from 'vue';const message = ref('Hello, Vue 3!');
const count = ref(0);const increment = () => {count.value++;
};
</script><style>
h1 {color: blue;
}
p {font-size: 16px;
}
button {background-color: green;color: white;padding: 5px 10px;cursor: pointer;
}
</style>
在这个示例中,我们使用
<script setup>
语法来定义组件的逻辑部分。通过import { ref } from 'vue';
引入ref
函数,然后直接在<script setup>
中定义message
和count
两个响应式数据,以及increment
方法。这样可以更加简洁地管理组件的状态和逻辑。
思考:为什么vue写成template形式而不是html
Vue.js 使用
template
而不是直接使用 HTML主要考虑了以下几个原因:
- 数据绑定:Vue.js 使用了数据绑定技术,可以动态地渲染和更新视图。如果直接使用 HTML,则需要使用一些特殊的语法来实现数据绑定,这会使 HTML 变得复杂和难以维护。
- 组件化:Vue.js 是一个基于组件的框架,可以将复杂的应用分解成多个小的组件。如果直接使用 HTML,则无法实现组件化的特性,因为 HTML 本身不支持组件的概念。
- 虚拟 DOM:Vue.js 使用了虚拟 DOM 技术,可以更高效地渲染和更新视图。如果直接使用 HTML,则需要使用一些特殊的语法来实现虚拟 DOM,这会使 HTML 变得复杂和难以维护。
- 编译优化:Vue.js 在编译期间可以对
template
进行优化,例如将相同的元素合并成一个元素,提高渲染性能。如果直接使用 HTML,则无法进行这种优化。
因此,Vue.js 使用
template
可以更好地支持数据绑定、组件化、虚拟 DOM 和编译优化等特性,同时可以让我们使用简单直观的方式来定义组件的结构和内容。
为什么render不直接将template转成html,而是转成了虚拟dom?
当我们使用
Vue
,往往会将页面拆分为各种组件,通过拼装组件来形成页面和应用,就像搭积木一样。模板编译后组件所产出的内容并不是html
字符串,而是大家所熟知的Virtual DOM。
组件最核心的东西是
render
函数,剩余的其他内容,如data
、compouted
、props
等都是为render
函数提供数据来源服务的。render
函数本可以直接产出html
字符串,但却产出了Virtual DOM
。Virtual DOM
终究要渲染真实 DOM,这个过程就可以理解为模板引擎年代的完全替换html
,只不过它采用的不是完全替换,我们通常把这个过程叫做patch。
当数据变更时,组件会产出新的VNode
,我们只需再次调用patch
函数即可
为何组件要从直接产出 html
变成产出 Virtual DOM
呢?
其原因是
Virtual DOM
带来了 分层设计,它对渲染过程的抽象,使得框架可以渲染到web
(浏览器) 以外的平台,以及能够实现SSR
等。至于Virtual DOM
相比原生 DOM 操作的性能,这并非Virtual DOM
的目标,确切地说,如果要比较二者的性能是要“控制变量”的,例如:页面的大小、数据变化量等
组件从直接产出 HTML 变成产出 Virtual DOM 的主要原因包括以下几点:
-
性能优化:Virtual DOM 可以作为一个轻量级的内存数据结构存在于内存中,通过对 Virtual DOM 进行比对,可以最小化对实际 DOM 的操作,从而提高性能。相比直接操作实际 DOM,Virtual DOM 的比对操作更高效,可以减少不必要的 DOM 操作,提升页面渲染性能。
-
跨平台兼容:Virtual DOM 的抽象层可以使得组件的渲染逻辑与具体平台无关,从而实现跨平台兼容。通过 Virtual DOM,可以将组件的渲染逻辑统一抽象,使得组件可以在不同平台上进行渲染,提高了组件的复用性和可移植性。
-
方便的状态管理:Virtual DOM 可以轻松地与状态管理库(如 Vuex、Redux 等)结合使用,实现组件状态的管理和更新。通过 Virtual DOM,可以更方便地管理组件的状态变化,实现数据驱动的视图更新。
-
简化复杂的 DOM 操作:直接操作实际 DOM 可能会涉及复杂的 DOM 操作,而 Virtual DOM 可以将这些操作抽象成简单的数据结构,使得组件的开发和维护更加简单和高效。
-
提高开发效率:通过 Virtual DOM,开发者可以更加专注于组件的逻辑和交互,而不需要过多关注底层的 DOM 操作细节。这样可以提高开发效率,减少开发成本。
综上所述,组件从直接产出 HTML 变成产出 Virtual DOM 主要是为了提高性能、跨平台兼容、方便的状态管理、简化复杂的 DOM 操作以及提高开发效率等方面的考虑。通过 Virtual DOM,可以更好地实现组件化开发和优化页面性能,提升用户体验。
编译VS运行时render
在大多数情况下,Vue会在编译阶段将模板(template)转换为渲染函数。这意味着Vue的编译器会在构建时将模板解析成渲染函数的静态代码,然后将这些静态代码打包到最终的构建文件中。编译时生成的渲染函数可以提高性能,因为它们是预先生成的静态代码,不需要在运行时进行解析和编译。
部分render函数在运行时生成:
- 在某些情况下,特别是在使用Vue的单文件组件(.vue文件)时,渲染函数可能会在运行时动态生成。这种情况下,Vue会在运行时解析模板并生成渲染函数。
- 在运行时动态生成渲染函数的过程中,Vue会利用编译器将模板转换为可执行的JavaScript代码,以便在每次组件渲染时动态生成虚拟DOM。这种方式相对于编译时生成静态渲染函数会带来一些性能开销,因为需要在每次渲染时进行模板解析和代码生成。
注:使用npm run dev,开发模式下虽然不会生成最终的生产构建文件,但是在使用
npm run dev
启动开发服务器时,Vue 项目中的代码仍然会经历编译和预处理过程,以便在开发服务器上实时编译和加载。
vue2的模板编译
实现模板编译共有三个阶段:解析、优化和生成
在vue2中,通过重写$mount方法,使得在调用原始的
$mount
函数之前,从template
选项中获取模板字符串或 DOM 元素,并将其编译为render
函数和staticRenderFns
数组。确保当前实例在调用$mount
函数时已经具有render
函数,从而可以正确地渲染到页面上。编译时生成的 render 函数会被挂载到组件实例的$options
对象上的render
属性中。这样,在组件实例化时,Vue 就可以直接从$options.render
中获取到预先编译好的 render 函数,而不需要每次都重新编译。
过程具体如下:
首先,Vue的编译器会将Vue模板(template)字符串解析为抽象语法树(Abstract Syntax Tree,AST)。AST表示了模板的结构和内容。编译器会对AST进行静态分析,识别模板中的静态内容(不会改变的部分)和动态内容(会改变的部分)。在静态分析的基础上,编译器会进行一些优化操作,例如静态节点提升(Static Node Hoisting)和静态属性提升(Static Props Hoisting),以减少渲染时的开销。根据AST和优化后的结果,编译器会生成对应的渲染函数代码。静态内容会被转换为静态的JavaScript代码,而动态内容会被转换为动态的JavaScript表达式。最终生成的渲染函数代码会被包含在最终的构建文件中,用于在组件渲染时生成虚拟DOM并更新页面。
render伪代码
重写$mount方法
const mount = Vue.prototype.$mount;//记录原$mount
Vue.prototype.$mount = function (el) {const vm = this;const options = vm.$options;el = document.querySelector(el);// 如果没有render方法if (!options.render) {let template = options.template;// 如果没有模板但是有elif (!template && el) {template = el.outerHTML;}const render = compileToFunctions(template);// 将render函数挂载到options上。options.render = render;}mount.call(this,..)//调用原$mount方法
}Vue.prototype._init = function (options) {const vm = this;vm.$options = options;// 初始化状态initState(vm);// 页面挂载if (vm.$options.el) {vm.$mount(vm.$options.el);}
}
compileToFunctions
export function compileToFunctions(template) {const root = parseHTML(template);let code = generate(root);let render = `with(this){return ${code}}`;let renderFn = new Function(render);return renderFn
}
vue2源码实现
重写$mount方法
首先,重写的$mount函数会检查
el
参数是否为字符串或 DOM 元素,如果不是,则会将其转换为字符串或 DOM 元素。如果el
参数为document.body
或document.documentElement
,则会打印一个警告信息,并返回当前实例。
接下来,这个$mount函数会检查当前实例的
$options
对象是否包含render
函数,如果不包含,则会尝试从template
选项中获取模板字符串或 DOM 元素,并将其编译为render
函数。如果template
选项为字符串,则会将其转换为模板字符串或 ID 选择器,并查找对应的 DOM 元素。如果template
选项为 DOM 元素,则会获取其innerHTML
属性。如果template
选项不存在,则会尝试从el
参数获取模板字符串或 DOM 元素。
如果
template
选项为字符串或 DOM 元素,则会使用compileToFunctions
函数将其编译为render
函数和staticRenderFns
数组。并且将render
函数和staticRenderFns
数组挂载到当前实例的$options
对象上。
compileToFunctions
函数是 Vue.js 中的编译器函数,它可以将模板字符串或 DOM 元素编译为render
函数和staticRenderFns
数组,并将其返回。最后,这个函数会调用原始的
$mount
函数,并将el
和hydrating
参数传递给它。
compileToFunctions方法
compileToFunctions
函数,它是将模板编译为 render 函数的入口函数。在这个函数中,Vue 的编译器会将模板解析成抽象语法树(AST),然后根据 AST 生成对应的 render 函数。
通过render函数反向去找怎么生成的
在 Vue.js 的源码中,createCompiler
函数是用于创建一个新的编译器的函数。createCompiler
函数调用createCompilerCreator函数,接收一个名为 baseCompile
的函数作为参数,并返回一个新的编译器函数。
当调用 createCompilerCreator函数时,会返回一个新的编译器函数,该函数包含 compile
和 compileToFunctions
两个方法。
compile
方法用于编译模板,并返回一个包含渲染函数和抽象语法树(AST)的对象。渲染函数是一个可以直接在渲染过程中使用的函数,而 AST 是模板的抽象语法树,可以用于进一步优化和代码生成。
compileToFunctions
方法也用于编译模板,但返回的是一个包含渲染函数和静态渲染函数数组的对象。这个方法可以用于将模板编译为可以直接在浏览器中运行的 JavaScript 函数。
baseCompile函数
baseCompile
是一个函数,在createCompiler
函数中被用作参数,返回一个新的编译器函数。baseCompile
函数的作用是将模板编译为抽象语法树(AST)并对其进行优化和代码生成。具体来说,编译器会遍历模板的 AST,根据不同的节点类型生成相应的代码片段,最终拼接成一个完整的 render 函数。这个 render 函数会在组件实例化时被调用,用于生成虚拟 DOM。
baseCompile
函数接收两个参数:
template
:要编译的模板字符串。options
:编译选项。
baseCompile
函数首先调用 parse
函数将模板解析为 AST,然后对 AST 进行优化,最后调用 generate
函数生成渲染函数和静态渲染函数数组。
在 parse
函数中,模板被解析为一个包含元素、指令和表达式的 AST。在 optimize
函数中,对 AST 进行优化以提高渲染性能。在 generate
函数中,根据 AST 生成渲染函数和静态渲染函数数组。
每个阶段具体如下:
1.模板解析-ast语法树
首先,Vue的编译器会将Vue模板(template)字符串解析为抽象语法树(Abstract Syntax Tree,AST)。AST表示了模板的结构和内容。
解析html的parse代码很长,几百行,它的作用就是将模板转换为ast语法树。给个示例看下ast语法树的样子吧
在AST explorer这个网站,可以看vue的模板转换为ast语法树的效果
AST (Abstract Syntax Tree),抽象语法树,记录了源代码的结构和语法信息。具体来说,AST 记录了以下信息:
- 节点类型:每个节点都有一个类型,例如表达式、函数、变量声明等。
- 节点内容:每个节点都有具体的内容,例如表达式的值、函数的名称和参数等。
- 子节点:每个节点可能有多个子节点,例如函数可能有多个参数,表达式可能有多个操作数。
- 位置信息:每个节点都有位置信息,包括行号和列号,用于定位源代码中的位置。
在 Vue.js 中,AST 记录了模板的结构和语法信息,包括以下内容:
- 元素:每个AST节点对应一个模板中的元素,包括标签、文本和注释。
- 属性:每个元素可能有多个属性,包括普通属性和指令属性。
- 表达式:元素和属性可能包含表达式,例如
v-if
指令中的表达式。- 位置信息:每个AST节点都有位置信息,包括行号和列号,用于定位模板中的位置。
通过记录这些信息,可以更好地理解和分析模板,并进行优化和代码生成,以提高渲染性能。
2.静态分析
编译器会对AST进行静态分析,识别模板中的静态内容(不会改变的部分)和动态内容(会改变的部分)。
3.优化
在静态分析的基础上,编译器会进行一些优化操作,例如静态节点提升(Static Node Hoisting)和静态属性提升(Static Props Hoisting)。优化器的作用是在AST中找出静态子树并打上标记。静态子树是在AST中永远不变的节点,如纯文本节 点,以减少渲染时的开销。
标记静态子树的好处:
- 每次重新渲染,不需要为静态子树创建新节点
- 虚拟DOM中patch时,可以跳过静态子树
4.代码生成
根据AST和优化后的结果,编译器会生成对应的渲染函数代码。静态内容会被转换为静态的JavaScript代码,而动态内容会被转换为动态的JavaScript表达式。
5.渲染函数输出
最终生成的渲染函数代码会被包含在最终的构建文件中,用于在组件渲染时生成虚拟DOM并更新页面。
vue2运行时动态生成render函数
在 Vue 2 中,当组件没有提供 render 函数且在编译阶段没有编译好的 render 函数时,那么在组件实例化时会动态生成 render 函数。这个 render 函数会在运行时生成,并且会被挂载到组件实例的
$options
对象上的render
属性中。
具体来说,Vue 会检查组件选项中的 render
方法。如果存在 render
方法,Vue 会调用该方法来动态生成 render 函数。这个 render
方法可以返回一个用于渲染组件的虚拟 DOM 树。
因此,在运行时动态生成 render 函数时,Vue 会调用组件实例的 $options.render
方法来生成 render 函数。这个方法的返回值将用于渲染组件的内容。
vue3的模板编译
Vue 3 在
render
函数方面相比 Vue 2 进行了一些改进和优化,在性能优化方面主要有两点改变
- 静态树提升(Static Tree Hoisting): Vue 3 在编译阶段会对模板进行静态分析,将静态节点提升为常量,减少渲染时的节点比对和更新操作,提高性能。
- 事件侦听器缓存(Event listener caching): Vue 3 会对事件侦听器进行缓存,避免每次渲染都重新创建事件侦听器,减少性能开销
vue3模板编译过程
parse 模板解析: 首先会调用
parse
方法,将模板源码解析为 AST(抽象语法树)的树形结构。AST 是对模板的抽象表示,方便后续的处理和转换。transform AST 转换: 接着可能会调用一系列的
transform
方法,对 AST 进行一些转换和优化操作。这些转换可以包括静态节点提升、插槽处理、指令转换等。generate 代码生成: 经过 AST 转换后,会调用
generate
方法,将经过处理后的 AST 节点转换为渲染函数的代码字符串。这个过程会将 AST 节点转换为可执行的 JavaScript 代码。优化代码: 可能会对生成的代码进行一些优化,例如进行尾部优化、静态节点提升等,以提高渲染函数的性能。
返回 CodegenResult: 最终将生成的代码字符串以及其他相关信息(如错误信息、提示等)封装在
CodegenResult
对象中返回,供后续使用。
render伪代码
compileToFunction伪代码
function compileToFunction(template, options) {const key = template;const cache = new Map();if (cache.has(key)) {return cache.get(key);}const { code } = compile(template, options); // 假设 compile 函数可以将模板编译成代码const render = new Function('Vue', code)(Vue); // 假设 Vue 是运行时的 Vue 实例// 标记函数为已编译render._rc = true;cache.set(key, render);return render;
}
function baseCompiler(source,options){const ast = baseParse(source,options);//生成ast语法树transform(ast,...)//对ast进行一个优化return generate(ast,options);//返回generate生成code
}
vue3源码实现
源码中,compileToFunction方法的实现位于core-main\packages\vue\src\index.ts文件中
compileToFunction方法
vue3模板编译的核心仍然是
compileToFunction
函数,将template模板转换成render函数。
整个过程可以分为以下几个步骤:
- 传入模板和选项参数: 函数接受两个参数,一个是模板
template
,另一个是选项参数options
。- 生成缓存键值: 将传入的模板作为键值
key
,用于缓存已经编译过的模板函数。- 检查缓存: 使用
cache
Map 对象来检查是否已经缓存了该模板对应的函数,如果有,则直接返回缓存的函数。- 模板编译: 调用
compile
函数对传入的模板进行编译,得到编译后的代码code
。这里假设compile
函数是一个能够将模板编译成代码的函数。- 创建函数: 使用
new Function
构造函数,将编译后的代码作为函数体,生成一个新的函数render
。这个函数在运行时将会接收一个 Vue 实例作为参数。- 执行函数: 调用生成的函数
render
,并传入 Vue 实例作为参数,得到最终的渲染函数。- 标记函数: 将生成的渲染函数标记为已编译,以便下次可以直接从缓存中获取。
- 缓存函数: 将生成的渲染函数存入缓存中,以备下次使用。
- 返回函数: 返回生成的渲染函数。
核心调用了compile方法,compile又调了baseCompile方法
baseCompiler方法
关键方法还是看baseCompile方法。在 Vue 3 的
baseCompile
函数中,主要完成了将模板源码编译为渲染函数的过程。
整个过程可以分为以下几个步骤:
parse 模板解析
首先会调用 parse
方法,将模板源码解析为 AST(抽象语法树)的树形结构。AST 是对模板的抽象表示,方便后续的处理和转换。
transform AST 转换
接着可能会调用一系列的 transform
方法,对 AST 进行一些转换和优化操作。这些转换可以包括静态节点提升、插槽处理、指令转换等。
generate 代码生成
经过 AST 转换后,会调用 generate
方法,将经过处理后的 AST 节点转换为渲染函数的代码字符串。这个过程会将 AST 节点转换为可执行的 JavaScript 代码。
优化代码
可能会对生成的代码进行一些优化,例如进行尾部优化、静态节点提升等,以提高渲染函数的性能。
返回 CodegenResult
最终将生成的代码字符串以及其他相关信息(如错误信息、提示等)封装在 CodegenResult
对象中返回,供后续使用。
如何查看打包后的render函数?
在 Vue 项目中,不论是编译时还是运行时,打包后的构建文件通常都会包含 render 函数。区别在于编译时生成的 render 函数是静态的,而运行时生成的 render 函数是动态的。
具体来说:
-
编译时生成的 render 函数:在编译阶段,Vue 的编译器会将模板(template)编译为静态的 render 函数。这个静态 render 函数会被包含在最终的构建文件中,用于在组件渲染时生成虚拟 DOM。
-
运行时生成的 render 函数:有时候,Vue 组件可能会在运行时动态生成 render 函数,特别是在动态组件或函数式组件的情况下。这些动态生成的 render 函数通常不会在编译时静态生成,而是在组件实例化或渲染时动态生成。
打包 Vue 项目时,通常会将 Vue 组件中的模板(template)转换为渲染函数(render function),然后将这些渲染函数打包到最终的构建文件中。如果想查看 render 函数被打包到哪里了,可以按照以下步骤进行:
查看构建输出文件:
- 查看生成的构建文件,通常在
dist
目录下。- 打开生成的构建文件,查找包含 Vue 组件代码的文件,通常是经过处理的 JavaScript 文件。
搜索渲染函数代码:
- 在构建文件中搜索 Vue 组件的代码,特别是包含
render
函数的部分。- 渲染函数通常会以函数的形式存在,搜索关键字如
render: function
或render() {
来找到渲染函数所在的位置
思考:一个template对应一个render函数吗?
在 Vue 中,一个 template 可以对应多个 render 函数。在 Vue 的编译过程中,一个 template 会被编译为一个 render 函数,这个 render 函数用于生成组件的虚拟 DOM。然而,有时候一个 template 也可以对应多个 render 函数,这取决于 Vue 组件的定义方式和使用场景。
-
单文件组件:在 Vue 单文件组件中,通常一个 template 会对应一个 render 函数。这是因为单文件组件中的 template 会被编译为一个 render 函数,并且在组件的定义中只会有一个 render 函数。
-
动态组件:在某些情况下,一个 template 可能会对应多个 render 函数,特别是在动态组件的情况下。动态组件可以根据不同的条件或状态渲染不同的组件,每个组件可能有不同的 template 和 render 函数。
-
函数式组件:在函数式组件中,通常不会有 template,而是直接使用 render 函数来定义组件的渲染逻辑。
思考:打包后的每个.js文件都会有render函数吗?
在 Vue 项目中,经过编译和打包后的每个
.js
文件并不一定都会包含 render 函数。Render 函数通常是在 Vue 单文件组件中定义的,然后经过编译器编译为 JavaScript 代码,并最终打包到构建文件中。
具体来说:
-
单文件组件:在 Vue 单文件组件中,通常会包含 template 和 render 函数。编译器会将 template 编译为 render 函数,并将这个 render 函数包含在最终的构建文件中。
-
普通 JavaScript 文件:在普通的 JavaScript 文件中,如果没有定义 Vue 组件或没有使用 render 函数,那么这些文件通常不会包含 render 函数。
-
动态组件和函数式组件:动态组件和函数式组件可能会在运行时动态生成 render 函数,这些 render 函数可能不会在打包后的每个
.js
文件中静态存在,而是根据需要动态生成。