系列文章目录
vue源码学习——初始化data
vue源码学习——响应式数据
前言
在vue中,我们一直使用的是template模板,而不是真正的html,所以我们才能在template模板中使用各种指令v-if,{{}}表达式等。但最终template需要经过编译过程,转化为真正的html语言,才能渲染成我们的页面~
一、Vue的模板语法
Vue官方介绍:
1、Vue.js 使用了基于 HTML 的模板语法,允许开发者声明式地将 DOM 绑定至底层 Vue 实例的数据。所有 Vue.js的模板都是合法的 HTML,所以能被遵循规范的浏览器和 HTML 解析器解析。
2、 在底层的实现上,Vue 将模板编译成虚拟 DOM 渲染函数。结合响应系统,Vue 能够智能地计算出最少需要重新渲染多少组件,并把 DOM操作次数减到最少。
3、如果你熟悉虚拟 DOM 并且偏爱 JavaScript 的原始力量,你也可以不用模板,直接写渲染(render) 函数
,使用可选的 JSX语法。
二、Vue的$mount
在new Vue()时,Vue.prototype._init()
方法,最终会调用vm.$mount(vm.$options.el)
进行DOM挂载。Vue.prototype.$mount
在多个文件中都有定义。
- Vue是一个跨平台的MVVM 框架,这里支持
web
和weex
,platforms目录 是Vue.js 的入口,2个目录代表了2个主要入口,分别运行在不同平台。 - 【platforms/web/rutime/index.js】中定义了基础的
Vue.prototype.$mount
方法,但是不带模板编译功能。在【platforms/web/entry-runtime-with-compiler.js】中重新定义了Vue.prototype.$mount
方法加入了模板编译功能(template生成render函数的过程)。
web/entry-runtime-with-compiler.js
import { compileToFunctions } from './compiler/index'
//....
const mount = Vue.prototype.$mount
// 重写了$mount方法,添加了模板编译功能
Vue.prototype.$mount = function (
el?: string | Element, // 挂载的元素,可以是字符串,也可以是 DOM 对象
hydrating?: boolean
): Component {
el = el && query(el) //查找元素
//不能挂载到body,html元素上, 因为挂载点是会被组件模板自身替换点, 显然body/html不能被替换
if (el === document.body || el === document.documentElement) {
return this
}
const options = this.$options
// 检查options是否有render,无则需要编译生成render函数
if (!options.render) { // 无render方法,把el或template进行编译,生成render方法。
let template = options.template
if (template) { // 无render,有template,用template内容compile解析
if (typeof template === 'string') { // template的类型是字符串
if (template.charAt(0) === '#') { // template是ID
template = idToTemplate(template)
}
} else if (template.nodeType) {
template = template.innerHTML // template的类型是元素节点,则使用该元素的 innerHTML 作为模板
} else { //template既不是字符串又不是元素节点
return this
}
} else if (el) { // 无render,无template,那么使用el元素的outerHTML作为模板内容
template = getOuterHTML(el)
}
if (template) {
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile')
}
// !!获取转换后的render函数与staticRenderFns,并挂在$options上
const { render, staticRenderFns } = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render // 编译生成的render函数挂载到options
options.staticRenderFns = staticRenderFns
// 非produceiton环境时统计编译器性能, config是全局配置对象
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile end')
measure(`vue ${this._name} compile`, 'compile', 'compile end')
}
}
}
// 调用上面公共的mount方法(web/runtime/index.js),options若一开始有render方法,直接到这,不需要编译过程。若一开始无,需要经过编译,生成render,并挂载到options中。
return mount.call(this, el, hydrating)
}
- 【platforms/web/entry-runtime-with-compiler.js】中的$mount方法,就是加了一个
render
方法的判断。判断new Vue(options)中options是否传入了render。 - 若传入了render,则直接调用【platforms/web/runtime/index.js】中定义的不带编译功能的
$mount
基础方法往下走。 - 若未传入render,对传入的template参数进行一系列的判断,最终调用
compileToFunctions
方法,进行编译,生成render,再赋给options:options.render = render; options.staticRenderFns = staticRenderFns;
compileToFunctions
传入的第一个参数就是模板字符串template,第二个参数是配置选项options。compileToFunctions
最后追溯到是在【src/compiler】目录中定义的,compiler目录里面都是编译相关内容。
// plaltforms/web/comipler/index.js
import { createCompiler } from 'compiler/index'
const { compile, compileToFunctions } = createCompiler(baseOptions)
三、compiler主要文件
四、compile过程
compileToFunctions追根溯源
import { createCompiler } from 'compiler/index'
const { compile, compileToFunctions } = createCompiler(baseOptions)
export { compile, compileToFunctions }
-
compileToFunctions(template,options,vm)
在【src/platform/web/compiler/index.js】目录中定义,这个函数写的过程很绕,但最终是返回了ast、render、staticRenderFns。 -
createCompiler
在【src/compiler/index.js】定义:
import { createCompilerCreator } from './create-compiler'
export const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
// 1、模板解析
const ast = parse(template.trim(), options)
if (options.optimize !== false) {
// 2、优化
optimize(ast, options)
}
// 3、代码生成
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
baseCompile
函数中才包含了主要的编译过程,分为3个主要步骤:parse、optimize、generate。
1、parse
- vue就是将模板代码映射为AST数据结构,进行语法解析。parse过程正是用正则等方式解析 template 模板中的标签、元素、文本、注释等数据,形成AST(抽象语法树)。
- ASTNode三种类型(在【flow/compile.js】中有定义):
declare type ASTNode = ASTElement | ASTText | ASTExpression
元素-ASTElement:
declare type ASTElement = {
type: 1;
tag: string;
attrsList: Array<ASTAttr>;
attrsMap: { [key: string]: any };
rawAttrsMap: { [key: string]: ASTAttr };
parent: ASTElement | void;
children: Array<ASTNode>;
//...
};
表达式-ASTExpression:{{}}
declare type ASTExpression = {
type: 2;
expression: string;
text: string;
tokens: Array<string | Object>;
static?: boolean;
ssrOptimizability?: number;
start?: number;
end?: number;
has$Slot?: boolean
};
文本-ASTText:
declare type ASTText = {
type: 3;
text: string;
static?: boolean;
isComment?: boolean;
ssrOptimizability?: number;
start?: number;
end?: number;
has$Slot?: boolean
};
2、optimize
- 遍历AST,找出静态节点并打标记;在进行patch的过程中,DOM-Diff算法会直接跳过静态节点,从而减少了比较的过程,优化了 patch 的性能。
export function optimize (root: ?ASTElement, options: CompilerOptions) {
if (!root) return
isStaticKey = genStaticKeysCached(options.staticKeys || '')
isPlatformReservedTag = options.isReservedTag || no
// first pass: mark all non-static nodes.
markStatic(root)
// second pass: mark static roots.
markStaticRoots(root, false)
}
markStatic(root)
递归标记每个节点是否为静态节点:node.static = isStatic(node)
,也是为 markStaticRoots 服务的,先把每个节点都处理之后,更方便找静态根节点。markStaticRoots(root, false)
标记区域静态根节点:node.staticRoot = true
3、generate
export function generate (
ast: ASTElement | void,
options: CompilerOptions
): CodegenResult {
const state = new CodegenState(options)
//核心部分,生成render表达式字符串主体
const code = ast ? genElement(ast, state) : '_c("div")'
return {
render: `with(this){return ${code}}`, //最外层用with(this)包裹
staticRenderFns: state.staticRenderFns //被标记为 staticRoot 节点的 VNode 就会单独生成 staticRenderFns
}
}
- 入参为之前2个步骤得到的AST,出参为render表达式、staticRenderFns函数
genElement(ast, state)
为生成render表达式字符串的核心函数。- 如官网介绍,下面render的实例:
<div>
<header>
<h1>I'm a template!</h1>
</header>
<p v-if="message">{{ message }}</p>
<p v-else>No message.</p>
</div>
render:
function anonymous(
) {
with(this){return _c('div',[_m(0),(message)?_c('p',[_v(_s(message))]):_c('p',[_v("No message.")])])}
}
staticRenderFns:
_m(0): function anonymous(
) {
with(this){return _c('header',[_c('h1',[_v("I'm a template!")])])}
}
render code的大致结构:
_c(
// 1、标签
'div',
//2、数据对象
{
attr:{...}
...
},
//3、子节点数组,循环其模型
[
_c(...)
]
)
_c
是在initState-initRender中定义,其实是createElement
方法。with(this)
中this指向的是proxy data对象。若code中使用了变量message
,会查找this.message
至此,得到了render函数字符串,compile过程结束~
总结
$options
中无render
函数的vue实例经过compileToFunctions(template,options,vm)
编译完,得到了render,staticRenderFns函数,并挂载到$options
中。继续下面的生成vnode流程。