new Vue({
render: h => h(App)
})
.vue
Everyone is familiar with this, calling render will get the virtual DOM corresponding to the incoming template ( file), so where does this render come from? How does it convert .vue
files into browser-recognizable code?
There are two ways how the render function comes from
- The first is to generate a render function through template compilation
- The second is that we define the render function in the component ourselves, which will skip the process of template compilation
This article will introduce these two types, as well as the detailed principles of the compilation process.
Understanding Template Compilation
We know that <template></template>
this is a template, not real HTML. Browsers don't recognize templates, so we need to compile it into native HTML that browsers recognize.
The main process of this piece is
- Extract the native HTML and non-native HTML in the template, such as bound attributes, events, instructions, etc.
- Generate a render function after some processing
- The render function generates the corresponding vnode from the template content
- Then go through the patch process ( Diff ) to get the vnode to be rendered into the view
- Finally, create a real DOM node based on vnode, that is, insert native HTML into the view to complete the rendering
Items 1, 2, and 3 above are the process of template compilation
So how does it compile and finally generate the render function?
Detailed explanation of template compilation - source code
baseCompile()
This is the entry function of template compilation, which receives two parameters
template
: is the template string to be convertedoptions
: It is the parameter required for conversion
There are three main steps in the compilation process:
- Template parsing: extract
<template></template>
the tag elements, attributes, variables and other information in the template through regularization and other methods, and parse it into an abstract syntax treeAST
- Optimization: traverse
AST
to find out the static nodes and static root nodes, and add tags - Code Generation: According to
AST
the generated rendering functionrender
These three steps correspond to three functions respectively, which will be introduced one by one later, first look at baseCompile
where they are called in the source code
export const createCompiler = createCompilerCreator(function baseCompile (
template: string, // 就是要转换的模板字符串
options: CompilerOptions //就是转换时需要的参数
): CompiledResult {
// 1. 进行模板解析,并将结果保存为 AST
const ast = parse(template.trim(), options)
// 没有禁用静态优化的话
if (options.optimize !== false) {
// 2. 就遍历 AST,并找出静态节点并标记
optimize(ast, options)
}
// 3. 生成渲染函数
const code = generate(ast, options)
return {
ast,
render: code.render, // 返回渲染函数 render
staticRenderFns: code.staticRenderFns
}
})
Just a few lines of code, three steps, calling three methods is very clear
Let's take a look at what is returned at the end, and then delve into the source code of the methods called in the above three steps, so that we can know more clearly what the three steps are to do
compile result
For example, there is such a template
<template>
<div id="app">{
{name}}</div>
</template>
Print the compiled result, which is the result of the above source code return, and see what it is
{
ast: {
type: 1,
tag: 'div',
attrsList: [ { name: 'id', value: 'app' } ],
attrsMap: { id: 'app' },
rawAttrsMap: {},
parent: undefined,
children: [
{
type: 2,
expression: '_s(name)',
tokens: [ { '@binding': 'name' } ],
text: '{
{name}}',
static: false
}
],
plain: false,
attrs: [ { name: 'id', value: '"app"', dynamic: undefined } ],
static: false,
staticRoot: false
},
render: `with(this){return _c('div',{attrs:{"id":"app"}},[_v(_s(name))])}`,
staticRenderFns: [],
errors: [],
tips: []
}
It doesn't matter if you don't understand, just pay attention to what the three steps mentioned above have done
ast
Field, which is generated in the first stepstatic
The field, that is, the tag, is added in the second step according toast
thetype
render
Field, which is generated in the third step
I have a general impression, and then look at the source code
1. parse()
Source address:src/complier/parser/index.js - 79行
This method is the main function of the parser, which extracts <template></template>
all the tag
, props
, and children
information in the template string through regularization and other methods, and generates an ast object with a corresponding structure
parse
takes two parameters
template
: is the template string to be convertedoptions
: It is the parameter required for conversion. It contains four hook functions, which are used toparseHTML
extract the parsed string and generate the correspondingAST
The core steps are as follows:
Call parseHTML
the function to parse the template string
- Parse to the start tag, end tag, text, and comments for different processing
- When encountering text information during parsing, call the text parser
parseText
function for text parsing - When an include filter is encountered during the parsing process, the filter parser
parseFilters
function is called for parsing
The results of each step of parsing are merged into an object (that is, the final AST)
The source code of this place is really too long, there are hundreds of lines of code, so I will just post a rough outline. If you are interested, you can take a look at the above. When parsing the text, the call will be added according to different types of nodes to mark chars()
the type
nodes AST
. Type, this attribute will be used in the next step of marking
type | AST node type |
---|---|
1 | element node |
2 | dynamic text node containing variables |
3 | Plain text nodes without variables |
2. optimize()
This function is AST
to find the static node and the static root node in it, and add a mark, so that the comparison of the static node will be skipped in the later patch
process, and a copy of the past will be directly cloned, thereby optimizing patch
the performance
The external function called in the function will not paste the code, the general process is like this
-
**mark static node(markStatic)**. It is to judge the type. The three types with values of 1, 2, and 3 are introduced above.
- The type value is 1: it is a node that contains child elements, set static to false and mark child nodes recursively until all child nodes are marked
- type value is 2: set static to false
- The type value is 3: it is a plain text node that does not contain child nodes and dynamic attributes. Set it to static = true, and this will be skipped when patching, and a copy will be cloned directly
-
**Mark static root nodes (markStaticRoots)**, the principle here is basically the same as that of marking static nodes, only nodes that meet the following conditions can be counted as static root nodes
- The node itself must be a static node
- must have child nodes
- Child nodes cannot have only one text node
Source address:src/complier/optimizer.js - 21行
export function optimize (root: ?ASTElement, options: CompilerOptions) {
if (!root) return
isStaticKey = genStaticKeysCached(options.staticKeys || '')
isPlatformReservedTag = options.isReservedTag || no
// 标记静态节点
markStatic(root)
// 标记静态根节点
markStaticRoots(root, false)
}
3. generate()
This is the function that generates render, which means that it will eventually return something like the following
// 比如有这么个模板
<template>
<div id="app">{
{name}}</div>
</template>
// 上面模板编译后返回的 render 字段 就是这样的
render: `with(this){return _c('div',{attrs:{"id":"app"}},[_v(_s(name))])}`
// 把内容格式化一下,容易理解一点
with(this){
return _c(
'div',
{ attrs:{"id":"app"} },
[ _v(_s(name)) ]
)
}
Does this structure look familiar?
If you understand the virtual DOM, you can see that the above render is exactly the structure of the virtual DOM, which is to divide a label into tag
, props
, children
, there is nothing wrong
Before looking at generate
the source code, we need to understand render
what the last returned field above means, and then look at generate
the source code, it will be much easier. Otherwise, we don’t even know what the function returns. How can we understand this function? Woolen cloth
render
Let's translate the compiled above render
This is introduced in the first volume of with
" JavaScript You Don't Know ", which is a keyword used to cheat lexical scope, which allows us to refer to multiple properties on an object faster
see an example
const name = '掘金'
const obj = { name:'沐华', age: 18 }
with(obj){
console.log(name) // 沐华 不需要写 obj.name 了
console.log(age) // 18 不需要写 obj.age 了
}
with(this){}
The above is this
the current component instance. Because by with
changing the pointer of the attribute in the lexical scope, it is name
necessary to use it directly in the label, instead of this.name
this
What is that _c
, _v
and ?_s
This is defined in the source code, the format is: ** _c
(abbreviation) = createElement
(function name)**
Source address:src/core/instance/render-helpers/index.js - 15行
// 其实不止这几个,由于本文例子中没有用到就没都复制过来占位了
export function installRenderHelpers (target: any) {
target._s = toString // 转字符串函数
target._l = renderList // 生成列表函数
target._v = createTextVNode // 创建文本节点函数
target._e = createEmptyVNode // 创建空节点函数
}
// 补充
_c = createElement // 创建虚拟节点函数
Let's see if it becomes clearer
with(this){ // 欺骗词法作用域,将该作用域里所有属姓和方法都指向当前组件
return _c( // 创建一个虚拟节点
'div', // 标签为 div
{ attrs:{"id":"app"} }, // 有一个属性 id 为 'app'
[ _v(_s(name)) ] // 是一个文本节点,所以把获取到的动态属性 name 转成字符串
)
}
Next, let's look at generate()
the source code
generate
Source address:src/complier/codegen/index.js - 43行
This process is very simple, only a few lines of code, is to first judge AST
whether it is empty, if not empty, create a vnode according to the AST, otherwise create a vnode with an empty div
export function generate (
ast: ASTElement | void,
options: CompilerOptions
): CodegenResult {
const state = new CodegenState(options)
// 就是先判断 AST 是不是为空,不为空就根据 AST 创建 vnode,否则就创建一个空div的 vnode
const code = ast ? (ast.tag === 'script' ? 'null' : genElement(ast, state)) : '_c("div")'
return {
render: `with(this){return ${code}}`,
staticRenderFns: state.staticRenderFns
}
}
It can be seen that it is mainly genElement()
created through methods vnode
, so let's take a look at its source code to see how it was created
genElement()
Source address:src/complier/codegen/index.js - 56行
The logic here is still very clear, that is, a bunch of if/else
judgments on the attributes of the AST element nodes passed in to execute different generation functions
It can also be found here that another knowledge point, v-for, has a higher priority than v-if, because the for is judged first.
export function genElement (el: ASTElement, state: CodegenState): string {
if (el.parent) {
el.pre = el.pre || el.parent.pre
}
if (el.staticRoot && !el.staticProcessed) {
return genStatic(el, state)
} else if (el.once && !el.onceProcessed) { // v-once
return genOnce(el, state)
} else if (el.for && !el.forProcessed) { // v-for
return genFor(el, state)
} else if (el.if && !el.ifProcessed) { // v-if
return genIf(el, state)
// template 节点 && 没有插槽 && 没有 pre 标签
} else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
return genChildren(el, state) || 'void 0'
} else if (el.tag === 'slot') { // v-slot
return genSlot(el, state)
} else {
// component or element
let code
// 如果有子组件
if (el.component) {
code = genComponent(el.component, el, state)
} else {
let data
// 获取元素属性 props
if (!el.plain || (el.pre && state.maybeComponent(el))) {
data = genData(el, state)
}
// 获取元素子节点
const children = el.inlineTemplate ? null : genChildren(el, state, true)
code = `_c('${el.tag}'${
data ? `,${data}` : '' // data
}${
children ? `,${children}` : '' // children
})`
}
// module transforms
for (let i = 0; i < state.transforms.length; i++) {
code = state.transforms[i](el, code)
}
// 返回上面作为 with 作用域执行的内容
return code
}
}
The generation functions called by each type are not listed one by one. In general, there are only three types of vnode nodes created at last, element nodes, text nodes, and comment nodes.
custom render
Let me give you an example, the three situations are as follows
// 1. test.vue
<template>
<h1>我是沐华</h1>
</template>
<script>
export default {}
</script>
// 2. test.vue
<script>
export default {
render(h){
return h('h1',{},'我是沐华')
}
}
</script>
// 3. test.js
export default {
render(h){
return h('h1',{},'我是沐华')
}
}
The above three, the final rendering is exactly the same , because this h
is the one after the above template is compiled _c
At this time, someone may ask, why write it yourself, isn't it automatically generated by template compilation?
Good question! It is definitely beneficial to write your own
- If you write the vnode yourself, you will skip the template compilation directly, and you don’t need to parse the dynamic attributes, events, instructions, etc. in the template, so the performance will be slightly improved. This is reflected in the priority of the rendering below
- There are also some situations that can make our code writing more flexible, more convenient and concise, and will not be redundant
For example, Element-UI
there are a large number of directly written render functions in the source code of the components inside.
Next, let's see how these two points are reflected.
1. Rendering priority
First look at the part about template compilation in the life cycle of the official website
As you can see from the picture, if there is template
, it will not be taken care el
of, so template has a higher priority than el , for example
So what if we wrote our own render?
<div id='app'>
<p>{
{ name }}</p>
</div>
<script>
new Vue({
el:'#app',
data:{ name:'沐华' },
template:'<div>掘金</div>',
render(h){
return h('div', {}, '好好学习,天天向上')
}
})
</script>
After this code is executed, the page is rendered with only <div>好好学习,天天向上</div>
It can be concluded that the render function has a higher priority
Because no matter it is el
mounted, template
it will be compiled into render
a function in the end, and if there is already render
a function, the previous compilation will be skipped
This is also reflected in the source code
Find the answer in the source code:dist/vue.js - 11927行
Vue.prototype.$mount = function ( el, hydrating ) {
el = el && query(el);
var options = this.$options;
// 如果没有 render
if (!options.render) {
var template = options.template;
// 再判断,如果有 template
if (template) {
if (typeof template === 'string') {
if (template.charAt(0) === '#') {
template = idToTemplate(template);
}
} else if (template.nodeType) {
template = template.innerHTML;
} else {
return this
}
// 再判断,如果有 el
} else if (el) {
template = getOuterHTML(el);
}
}
return mount.call(this, el, hydrating)
};
2. More flexible writing
For example, when we need to write a lot of if judgments
<template>
<h1 v-if="level === 1">
<a href="xxx">
<slot></slot>
</a>
</h1>
<h2 v-else-if="level === 2">
<a href="xxx">
<slot></slot>
</a>
</h2>
<h3 v-else-if="level === 3">
<a href="xxx">
<slot></slot>
</a>
</h3>
</template>
<script>
export default {
props:['level']
}
</script>
I don't know if you have written code similar to the above?
Let's write the same code as above in another way, just write render
<script>
export default {
props:['level'],
render(h){
return h('h' + this.level, this.$slots.default())
}
}
</script>
Done! That's it! That's it?
That's right, that's it!
Or the following is very convenient when calling multiple times
<script>
export default {
props:['level'],
render(h){
const tag = 'h' + this.level
return (<tag>{this.$slots.default()}</tag>)
}
}
</script>