在日常的开发中,我们会把template之间的类似于HTML的内容成为模板。我们在模板中会写html代码、以及一些v-for、v-if等指令等。那么在vue中是怎么处理这些非html结构的代码的呢?最终渲染页面的这个过程又是怎样的呢?这里先用一段话总结一下。
Vue会识别template中的内容,划分为原生html部分进行编译以及非原生html部分,对于非原生html部分,则会通过一系列逻辑处理成render函数,render函数则会根据模板内容生成对应的Vnode。Vnode通过patch过程得到要渲染到页面的Vnode,最后根据Vnode创建真实的Dom节点并且插入到视图中。从而实现页面的渲染。
这个过程也就是我们所讲的Vue模板编译。如下图:
接下来,我们把问题拆分一下一步一步去理解模板编译的过程。
我们可以提出一个问题:
模板编译内部是如何处理模板形成render函数的呢?
要弄明白这个首先得明白一个东西 ——抽象语法树(AST),通过js对象来描述模板的属性、文本等。具体长什么样子可以通过这个去实现一下:在线解析AST
这里用一段话大致总结一下这个过程:
生成render函数一共有三个阶段,第一个阶段是模板解析阶段,将模板字符串用正则的方式解析成抽象语法树(AST)。第二个阶段是优化阶段,遍历AST,对于静态节点打上标记。第三个阶段是代码生成阶段,将AST转化为render函数。过程源码如下:
// 源码位置: /src/complier/index.js
export const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
// 模板解析阶段:用正则等方式解析 template 模板中的指令、class、style等数据,形成AST
const ast = parse(template.trim(), options)
if (options.optimize !== false) {
// 优化阶段:遍历AST,找出其中的静态节点,并打上标记;
optimize(ast, options)
}
// 代码生成阶段:将AST转换成渲染函数;
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
图例:
现在已经知道了大概的过程,开始分步理解三个阶段分别做了哪些事情。
1、解析阶段
主要的源码:
// 代码位置:/src/complier/parser/index.js
/**
* Convert HTML string to AST.
* 将HTML模板字符串转化为AST
*/
export function parse(template, options) {
// ...
parseHTML(template, {
warn,
expectHTML: options.expectHTML,
isUnaryTag: options.isUnaryTag,
canBeLeftOpenTag: options.canBeLeftOpenTag,
shouldDecodeNewlines: options.shouldDecodeNewlines,
shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,
shouldKeepComment: options.comments,
// 当解析到开始标签时,调用该函数
start (tag, attrs, unary) {
},
// 当解析到结束标签时,调用该函数
end () {
},
// 当解析到文本时,调用该函数
chars (text) {
},
// 当解析到注释时,调用该函数
comment (text) {
}
})
return root
}
可知,传入两个参数,template是要被解析的HTML模板字符串,option是转换时所需的选项。 除此之外,还定义了一些钩子函数。当解析到不同的节点时候,调用不同的钩子函数。
钩子函数具体的实现细节可以查阅源码。原理都是通过正则匹配创建不同的AST结构类型。
为了防止重复解析,解析器中提供一个游标控制器,伪代码如下:
function advance (n) {
index += n // index为解析游标
html = html.substring(n)
}
但是这里有一个问题,我们都知道html中的标签往往是成对存在的,标签与标签之间存在层级的关系,那如果按照这种解析的方式,如何能保证解析出正确的ast树呢?其实,在vue中解析器的最开始会初始化一个stack(栈)用来维护这个节点层级。当遇到标签的开始
2、优化阶段
上面初步了解了模板编译中的解析阶段所做的事情。进入到第二部分优化阶段。这一阶段主要是vue优化性能的。当模板中存在静态节点(首次渲染后不会再改变的节点)的时候,在这个阶段会给他们打上标记,在diff阶段、patch的时候则会跳过对这些节点的判断从达到性能优化的目的。
具体来说,在优化阶段中主要做了两件事。
第一件:遍历AST,对所有的静态节点打上标记。
第二件:遍历AST,对所有静态根节点打上标记。
代码如下:
export function optimize (root: ?ASTElement, options: CompilerOptions) {
if (!root) return
isStaticKey = genStaticKeysCached(options.staticKeys || '')
isPlatformReservedTag = options.isReservedTag || no
// 标记静态节点
markStatic(root)
// 标记静态根节点
markStaticRoots(root, false)
}
具体过程这里不细讲,主要就是通过递归的方式去实现标记。
3、代码生成阶段
在Vue中,vue实例在挂载的时候会调用自身的render函数,来生成模板对应的Vnode。当我们在书写组件的时候,如果在代码中制指定了render选项,vue实例挂载的时候会调用自定义的render函数去进行生成Vnode。如果没有指定render选项,vue会自动根据template生成一个render函数供程序调用。
render函数的书写在后面会单独出一篇文章,这里不再扩展。
这里简单了解一下vue是如何自动根据template生成render函数的。
源码:
export function generate (ast,option) {
const state = new CodegenState(options)
const code = ast ? genElement(ast, state) : '_c("div")'
return {
render: `with(this){return ${code}}`,
staticRenderFns: state.staticRenderFns
}
}
我们可以知道Vnode有很多不同的类型,比如文本节点,注释节点,元素节点等,通过递归生成好的AST语法树,当遇到是文本类型就调用生成文本节点的方式创建文本类型的Vnode,当遇到元素类型则调用生成元素类型的Vnode的方式生成元素类型的Vnode。以此类推。最终生成一份完整的Vnode。这里只讲述了大概的过程,生成虚拟dom我后面会写一篇文章来讲述虚拟Dom以及diff算法。