编译:编译就是把高级语言变成计算机可以识别的2进制语言, 但是我们这里所说的Vue的编译指的是: 把template模板字符串转换成render渲染函数的过程。
通过上面所说的我们就应该知道看源码的什么地方了:应该就是定义render函数的地方了, 通常我们使用Vue的话有两种方式:
-
自己定义render函数: new Vue({ render:h=>h(App) }).$mount("#root")
-
使用template new Vue({ template:"
Hello World!" }).$mount("#root")
通过上面的使用方法我们可以看到不管自定义render还是直接使用template模板方式,最后都需要调用$mount方法,
mount方法的定义:
// entry-runtime-with-compiler.js
const mount = Vue.prototype.$mount
// 带编译器的$mount
// 也就是说如果我们new Vue的时候使用了template属性,就需要调用这个$mount函数
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
// 获取根节点
// query方法 如果document.querySelector(el)存在就直接返回,不存在就createElement("div")返回
el = el && query(el)
/* istanbul ignore if */
if (el === document.body || el === document.documentElement) {
process.env.NODE_ENV !== 'production' && warn(
`Do not mount Vue to <html> or <body> - mount to normal elements instead.`
)
return this
}
const options = this.$options
// resolve template/el and convert to render function
// 没有render函数
if (!options.render) {
// 获取template 模板字符串
let template = options.template
// 如果有的话 就执行。。。
if (template) {
// template:#app
// 获取页面中id为app的节点的innerHTML的内容
if (typeof template === 'string') {
if (template.charAt(0) === '#') {
// 获取页面中id为app的节点的innerHTML的内容
//类似这种用法: <script type="text/x-template" id="app">...</script>
template = idToTemplate(template)
}
} else if (template.nodeType) {
template = template.innerHTML
} else {
return this
}
} else if (el) {// 没有template 的话 获取el.outerHTML
template = getOuterHTML(el)
}
if (template) {
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
mark('compile')
}
// 根据template模板生成render函数
const { render, staticRenderFns } = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== 'production', // 参数啥意思?
shouldDecodeNewlines, // 这两个参数应该是判断节点的参数会不会在浏览器中被转义的
shouldDecodeNewlinesForHref,
delimiters: options.delimiters, // 改变纯文本插入分隔符。<span>{{这里的"{{ }}"应该就是分割符 }}}}</span>
comments: options.comments // 当设为 true 时,将会保留且渲染模板中的 HTML 注释。默认行为是舍弃它们。
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
}
}
// 最后调用不包括compiler的mount方法
return mount.call(this, el, hydrating)
}
通过上面的代码我们可以看出来转换成render函数的方法就是 compileToFunctions 这个方法,下面我们来看一下具体的流程是怎样的:
我们根据流程图来大概解释一下:
- creatCompiler函数用来生成complie和compileToFunctions函数
- complie函数调用baseComplie函数来生成render字符串函数体(baseComplie转换成的render为字符串函数体,类似这种:"with(this){return _c('div',[_c('h1',[_v("我是父组件")])],2)}")
- complieToFunction函数用来把render字符串函数体转换为render函数
下面我们在结合代码来看一下这个逻辑,基本上就是几个高阶函数的相互调用:
// platforms/web/compiler/index.js --- compileToFunctions函数
import { createCompiler } from 'compiler/index'
const { compile, compileToFunctions } = createCompiler(baseOptions)
// compiler/index.js -- createCompiler函数
import { createCompilerCreator } from './create-compiler'
// createCompilerCreator 高阶函数 返回另一个函数
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转换成render字符串函数体
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
// compiler/create-compiler.js --- createCompilerCreator函数
export function createCompilerCreator (baseCompile: Function): Function {
// 返回createCompiler函数
return function createCompiler (baseOptions: CompilerOptions) {
function compile (
template: string,
options?: CompilerOptions // 具体里面有哪些参数,请查看CompilerOptions类型定义的地方(flow文件夹里面有)
): CompiledResult {
const finalOptions = Object.create(baseOptions)
// 合并一下配置参数
if (options) {
// merge custom modules
if (options.modules) {
finalOptions.modules =
(baseOptions.modules || []).concat(options.modules)
}
// merge custom directives
if (options.directives) {
finalOptions.directives = extend(
Object.create(baseOptions.directives || null),
options.directives
)
}
// copy other options
for (const key in options) {
if (key !== 'modules' && key !== 'directives') {
finalOptions[key] = options[key]
}
}
}
finalOptions.warn = warn
// 调用baseCompile函数
const compiled = baseCompile(template.trim(), finalOptions)
compiled.errors = errors
compiled.tips = tips
return compiled
}
return {
compile,
compileToFunctions: createCompileToFunctionFn(compile)
}
}
}
// compiler/to-function.js createCompileToFunctionFn函数
export function createCompileToFunctionFn (compile: Function): Function {
// 闭包的方式缓存不同模板的render函数
const cache = Object.create(null)
return function compileToFunctions (
template: string,
options?: CompilerOptions,
vm?: Component
): CompiledFunctionResult {
options = extend({}, options)
const warn = options.warn || baseWarn
delete options.warn
// check cache
// key = "{,}<div>
// <h1>我是父组件</h1>
// </div>
// "
const key = options.delimiters
? String(options.delimiters) + template
: template
// 如果已经存在了就直接返回
if (cache[key]) {
return cache[key]
}
// compile
const compiled = compile(template, options)
// turn code into functions
const res = {}
const fnGenErrors = []
// complied.render:"with(this){return _c('div',[_c('h1',[_v("我是父组件")]),_v(" "),_c('h2',[_v("msg:"+_s(msg))])])}" 他是这个东西。。
// createFunction方法就是把这个字符串转换成函数,
res.render = createFunction(compiled.render, fnGenErrors)
...
return (cache[key] = res)
}
}
通过上面的讲解我们应该清楚将template转换成render渲染函数的逻辑主要是在baseComplier函数中的,下面我们来重点讲解一下这个函数:
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
}
}
可以看到里面的逻辑也比较清晰,大致分为三个流程:
-
parse函数:把我们写的模板字符串(template)中的节点通过js对象的方式来描述出来,用于后面来生成对应render函数
-
optimize函数:遍历我们上面得到的astElement对象,然后找出其中的静态节点/静态根节点,并打上标记
-
generate函数: 根据上面的对象生成render函数(用于生成VNode节点)
下面我们来逐一讲解一下每个函数的具体逻辑:
parse函数
我们来看一下parse函数的代码,看见里面主要的函数就是parseHTML函数,下面我们来结合代码看一下具体逻辑:
// compiler/parser/index.js
export function parse (
template: string,
options: CompilerOptions
): ASTElement | void {
// 标识符
delimiters = options.delimiters
const stack = []
let root
let currentParent
let inVPre = false
let inPre = false
let warned = false
parseHTML(template, {
warn,
expectHTML: options.expectHTML,
isUnaryTag: options.isUnaryTag,
canBeLeftOpenTag: options.canBeLeftOpenTag,
shouldDecodeNewlines: options.shouldDecodeNewlines,
shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,
shouldKeepComment: options.comments,
outputSourceRange: options.outputSourceRange,
/*
tag:标签名
attrs:标签属性
unary:是否是自闭合标签
start:开始索引
end:结束索引
*/
start (tag, attrs, unary, start, end) {
// 创建一个tag类型的AST节点
let element: ASTElement = createASTElement(tag, attrs, currentParent)
// v-for指令
processFor(element)
// v-if指令
processIf(element)
//v-once指令: el.once = true
processOnce(element)
// root是根节点,第一次调用start钩子函数的时候 默认是root,后面再调用的时候会通过下面的closeElement方法添加到root.children中
if (!root) {
root = element
}
// 非自闭合标签,
// currentParent:当前的节点
if (!unary) {
currentParent = element
stack.push(element)
} else {
closeElement(element)
}
},
end (tag, start, end) {
const element = stack[stack.length - 1]
// pop stack
stack.length -= 1
currentParent = stack[stack.length - 1]
closeElement(element)
},
chars (text: string, start: number, end: number) {
const children = currentParent.children
if (text) {
let res
let child: ?ASTNode
// {{item}} 走这个节点
if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) {
child = {
type: 2,
expression: res.expression,
tokens: res.tokens,
text
}
} else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {
child = {
type: 3,
text
}
}
if (child) {
children.push(child)
}
}
},
comment (text: string, start, end) {
if (currentParent) {
const child: ASTText = {
type: 3,
text,
isComment: true
}
currentParent.children.push(child)
}
}
})
return root
}
通过上面我们可以看到parse主要就是调用parseHTML函数,下面我们来通过一张图来理解一下parseHTML函数的作用: 首先我们能看到parseHTML函数调用的时候会有四个钩子函数:
- start函数
当正则匹配到开始标签的时候调用, 根据开始标签的tagName生成astElement节点对象 处理开始标签中的属性:v-for/v-if/attrs等, - end函数
正则匹配到结束标签的时候调用 调用closeElement方法 把当前结束的标签添加到对应父标签的children中 - chars函数
正则匹配到当前节点为文本节点时调用 生成文本astElement节点对象 - comment函数
正则匹配到注释节点时调用 生成注释节点对象
下面我们来通过一张图来理解一下具体template的编译过程:
总结一下:
parse函数就是把template转换成astElement节点
optimize函数
标记一下节点是否为静态节点/静态根节点
- 静态节点:
function isStatic (node: ASTNode): boolean {
if (node.type === 2) { // expression 指的是 <h1>{{msg}}</h2>中的{{msg}}节点
return false
}
if (node.type === 3) { // text
return true
}
return !!(node.pre || (
!node.hasBindings && // no dynamic bindings
!node.if && !node.for && // not v-if or v-for or v-else
!isBuiltInTag(node.tag) && // not a built-in
isPlatformReservedTag(node.tag) && // not a component
!isDirectChildOfTemplateFor(node) &&
Object.keys(node).every(isStaticKey) // 说明只包含基础的属性(像v-if/v-for、slot等都会给node增加属性的)
))
}
符合上面条件的节点就是静态节点
- 静态根节点:
function markStaticRoots (node: ASTNode, isInFor: boolean) {
if (node.type === 1) {
// 这个节点时静态节点,并且children 并不仅仅只有text文本
if (node.static && node.children.length && !(
node.children.length === 1 &&
node.children[0].type === 3
)) {
node.staticRoot = true
return
} else {
node.staticRoot = false
}
// 同样还要判断子节点里面是不是符合这个条件
if (node.children) {
for (let i = 0, l = node.children.length; i < l; i++) {
markStaticRoots(node.children[i], isInFor || !!node.for)
}
}
// if if-else if-else-if 等条件渲染的时候的 不显示的节点 同样的道理
if (node.ifConditions) {
for (let i = 1, l = node.ifConditions.length; i < l; i++) {
markStaticRoots(node.ifConditions[i].block, isInFor)
}
}
}
}
generate函数
把我们生成的astElement节点对象生成render字符串函数体:
具体的代码我们这里就不在详细讲述了,里面的代码逻辑不复杂,自己感兴趣的话可以看一下,我们来举例看一下我们的template转换之后生成的render字符串函数体:
//<div>
//<p v-for="(item,idx) in list" :key="idx">{{item}}</p> ---> "_l((list),function(item,idx){return _c('p',{key:idx},[_v(_s(item))])})"
// <h1>我是父组件</h1> ---> "_c('h1',[_v("我是父组件")])"
// <h2 ref="msgCon">msg:{{msg}}</h2> ---> "_c('h2',{ref:"msgCon"},[_v("msg:"+_s(msg))])"
// <p v-if="inpVal === 'abc'">我是v-if的元素,只有inpVal==='abc'的时候才会显示</p> ---> "_c('p',[_v("我是v-if的元素,只有inpVal==='abc'的时候才会显示")])"
// <h2>计算属性msgDouble: {{strDouble}}</h2> ---> "_c('h2',[_v("计算属性msgDouble: "+_s(strDouble))])"
// <input type="text" v-model="inpVal"/> ---> "_c('input',{directives:[{name:"model",rawName:"v-model",value:(inpVal),expression:"inpVal"}],attrs:{"type":"text"},domProps:{"value":(inpVal)},on:{"input":function($event){if($event.target.composing)return;inpVal=$event.target.value}}})"
// <ComponentA :foo='msg' @changeFoo="changeMsg"/> ---> "_c('ComponentA',{attrs:{"foo":msg},on:{"changeFoo":changeMsg}})"
//</div>
//我们template模板如下:
<template>
<div>
<p v-for="(item,idx) in list" :key="idx">{{item}}</p>
<h1>我是父组件</h1>
<h2 ref="msgCon">msg:{{msg}}</h2>
<p v-if="inpVal">我是v-if的元素,只有inpVal==='abc'的时候才会显示</p>
<h2>计算属性msgDouble: {{strDouble}}</h2>
<input type="text" v-model="inpVal"/>
<ComponentA :foo='msg' @changeFoo="changeMsg"/>
</div>
</template>
// 转换之后的render字符串函数体如下:
`with(this){
return _c('div',
[
_c('h1',[_v("我是父组件")]),
_v(" "),
_c('h2',{ref:"msgCon"},[_v("msg:"+_s(msg))]),
_v(" "),
(inpVal)?_c('p',[_v("我是v-if的元素,只有inpVal==='abc'的时候才会显示")]):_e(),
_v(" "),
_c('h2',[_v("计算属性msgDouble: "+_s(strDouble))]),
_v(" "),
_c('input',{directives:[{name:"model",rawName:"v-model",value:(inpVal),expression:"inpVal"}],attrs:{"type":"text"},domProps:{"value":(inpVal)},on:{"input":function($event){if($event.target.composing)return;inpVal=$event.target.value}}}),
_v(" "),
_c('ComponentA',{attrs:{"foo":msg},on:{"changeFoo":changeMsg}})
],2)
}`
//里面我们能够看到好多_c、_l等这种内部变量,_C对应的就是createElement函数,下面我们来看一下这些内部变量的定义:
export function installRenderHelpers (target: any) {
target._o = markOnce
target._n = toNumber
target._s = toString
target._l = renderList
target._t = renderSlot
target._q = looseEqual
target._i = looseIndexOf
target._m = renderStatic
target._f = resolveFilter
target._k = checkKeyCodes
target._b = bindObjectProps
target._v = createTextVNode
target._e = createEmptyVNode
target._u = resolveScopedSlots
target._g = bindObjectListeners
target._d = bindDynamicKeys
target._p = prependModifier
}
以上就是生成的render函数,我们后面渲染页面的时候就可以通过render函数生成VNode节点,然后使用Vue提供的update方法把节点渲染到页面中了。
总结
别着急慢慢看,没看明白就缓缓再看,多看几遍总会明白的。