vue项目编译,webpack编译速度优化,webpack编译
vue实例生成vnode是基于render函数,但平时我们很少直接写render函数,而是写template,vue会通过编译将template转化成render函数。
在vue包的package.json中,"module"
字段指向的是"dist/vue.runtime.esm.js"
,所以通过import Vue from "vue"
引入的vue是运行时runtime的代码,不包含编译部分,这样写会报错。
import Vue from "vue"
var vm = new Vue({
el: '#root',
template: '
test
'
})
能在.vue文件中使用template是因为vue-loader在构建的时候将template编译成了render函数。
要debug vue的编译过程可以在webpack配置文件中改变vue的引用。
module.exports = {
...
resolve: {
alias: {
'vue': path.join(__dirname,"node_modules/vue/dist/vue.esm.js")
}
}
}
也可以直接在html文件中引入带编译器的vue版本,不用webpack。
var vm = new Vue({
el: '#root',
template: 'test'
})
编译入口
Vue 项目中的platform/web下的 entry-runtime.js 文件是 Vue 用于构建仅包含运行时的源码文件,而 entry-runtime-with-compiler.js 是用于构建同时包含编译器和运行时的源码文件。
对比运行时的entry-runtime.js可以看出,entry-runtime-with-compiler.js扩展了Vue.prototype.$mount
方法,将编译相关的工作都封装在了这个方法里。
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (el,hydrating) {
el = el && query(el)
const options = this.$options //this.$options是new Vue(options)传入的options
//如果有render函数,就跳过这一段直接执行mount方法
if (!options.render) {
let template = options.template
if (template) { //如果没有render函数,有template就用template
...
} else if (el) { //如果没有render函数,也没有template,就用el生成template
template = getOuterHTML(el)
}
if (template) { //将template生成编译生成render函数,并赋值给this.$options
const { render, staticRenderFns } = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
}, this)
options.render = render
options.staticRenderFns = staticRenderFns
}
}
return mount.call(this, el, hydrating)
}
所以如果既有render函数又有template,就会直接使用render,忽略template。
生成render函数的是compileToFunctions方法。compileToFunctions方法又是一系列高阶函数生成的,将参数传递解耦。
compileToFunctions(template,options,vm)
const {compileToFunctions} = createCompiler(baseOptions)
const createCompiler = createCompilerCreator(baseCompile)
function createCompilerCreator (baseCompile){
return function createCompiler (baseOptions) {
function compile(template, options)){
baseCompile(template.trim(), finalOptions)
}
return {
compile,
compileToFunctions: createCompileToFunctionFn(compile)
}
}
}
function createCompileToFunctionFn(compile){
return function compileToFunctions (template,options,vm){
compile(template, options)
}
}
函数执行的顺序是:createCompilerCreator -> createCompiler -> createCompileToFunctionFn -> compileToFunctions -> compile -> baseCompile
执行compileToFunctions(template,options,vm)
方法,最终会执行baseCompile(template.trim(), finalOptions)
方法,它也是编译的核心函数。
编译核心流程
baseCompile
方法定义如下:
function baseCompile (template,options){
const ast = parse(template.trim(), options)
if (options.optimize !== false) {
optimize(ast, options)
}
const code = generate(ast, options)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
}
它的结构很清晰,分为三步,分别由parse、optimize、generate完成。
- 1、将模板字符串解析为AST
- 2、优化AST
- 3、将AST转换成render函数
从字符串到AST
AST是什么
AST,是抽象语法树(Abstract Syntax Tree)的缩写,是源代码的抽象语法结构的树状表现形式。在这里,源代码就是指template中的代码,而将要生成的AST是一个javascript对象,它可以描述template中代码的结构。
以这样的一个template为例
new Vue({
el: '#root',
template: '
{{item}}
test
',
data() {
return {
list : ['aaa','bbb','ccc']
}
}
})
它生成的AST如下。
AST是一个树形结构的对象,和dom树类似。比如这颗树的根节点就是最外层的div,对应上面模板的内容我们可以看到它的tag: "div", attrsMap: {class: "tpl"}
, 它的children
有两个,对应两个p标签
,p标签
的结构和div
相同。再往下可以看到p标签
的children
。
它们在template中对应的部分分别是{{item}}
和test
,它们的结构和div
、p
不同,一个是表达式节点,一个是文本节点。template编译一共有三种节点:
- 1、元素节点,type为1
- 2、表达式文本节点,type为2
- 3、普通文本节点, type为3
生成AST的流程
const ast = parse(template.trim(), options)
parse
方法的整体过程:
function parse(template,options){
...//解析options
let root
parseHTML(template, {
warn,
//some options...此处省略
/**解析过程中的回调函数**/
start(){}, -->解析开始标签时调用
end(){}, -->解析结束标签时调用
chars(){}, -->解析文本时调用
comment(){}, -->解析注释时调用
})
return root
}
parse
方法的主体是parseHTML
, parseHTML
要做的可以概括为两件事:
1、用正则表达式匹配出开始标签、结束标签、文本、注释等内容
2、在匹配出这些内容后,结合各自对应的回调函数进行处理,生成AST节点
parseHTML
的整体流程是循环解析template,用正则做匹配,根据匹配情况做不同的处理,直到整个template解析完。
function parseHTML (html, options) {
let index = 0
let lastTag //上一次匹配的得到的标签
function advance (n) { //推进向前,得到下一次要解析的html
index += n
html = html.substring(n)
}
while(html){
if(!lastTag || !isPlainTextElement(lastTag)){ //不在script和style标签中
let textEnd = html.indexOf('<')
if(textEnd === 0){ //html的第一个字符是"<", 分下面几种情况,下面是伪代码
//1、注释
if(isComment){
if (options.shouldKeepComment) {
options.comment() //如果保留注释,执行options传入的注释回调
}
advance (commentLength)
continue
}
//2、条件注释 <![if !IE]> <![endif]>
if(isConditionalComment){
advance (conditionalCommentLength)
continue
}
//3、doctype <!DOCTYPE html>
if(isDoctype){
advance (doctypeLength)
continue
}
//4、结束标签
if(isEndTag){
advance(endTag.length)
parseEndTag()
continue
}
//5、开始标签
if(isStartTag){
parseStartTag() //advance是在parse的过程中执行
handleStartTag()
continue
}
}
//"<"字符不在第一个位置
if (textEnd >= 0) { 如abc< , abc<<< , abc<<<div>, abc<div> , abc等
text = extractedText 对应上面分别是abc< , abc<<< , abc<<, abc , abc
}
//纯文本,没有"<"
if (textEnd < 0) {
text = html
}
advance(text.length)
options.chars() //执行options传入的文本回调
}else{//如果在script或style标签中
handlePlainTextElement()
advance(plainTextElementLength)
parseEndTag()
}
}
}
解析开始标签
const startTagMatch = parseStartTag()
if (startTagMatch) {
handleStartTag(startTagMatch)
if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) {
advance(1)
}
continue
}
解析开始标签分两步:
1、由parseStartTag方法解析html,拿到匹配到的结果
2、调用handleStartTag方法处理匹配到的结果
function parseStartTag () { const start = html.match(startTagOpen) // ["<div","div",index:0,...] if (start) { //创建一个对象,保存开始标签中的信息 const match = { tagName: start[1], attrs: [], //保存所有属性的数组 start: index } advance(start[0].length) let end, attr //在匹配到开始标签的">" 或 "/>"前,遍历匹配开始标签中的属性 //attribute匹配普通属性,dynamicArgAttribute匹配动态属性,这个正则略复杂,可以借助正则工具看 const attribute = /^\s*([^\s"'\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=`]+)))?/ const dynamicArgAttribute = /^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/ while (!(end = html.match(startTagClose)) && (attr = html.match(dynamicArgAttribute) || html.match(attribute))) { //arrr = [" class="tpl"", "class", "=", "tpl", undefined, undefined,index:0,...] attr.start = index advance(attr[0].length) attr.end = index match.attrs.push(attr) } if (end) { //end = [">", "", index:0, ...] match.unarySlash = end[1] //判断是否是自闭合标签,如<img /> advance(end[0].length) match.end = index return match } } }
解析
<div class="tpl">
得到的对象,主要的信息就是:标签名tagName,属性数组attrs,是否是自闭合标签unarySlash。 下一步是传入这个对象,执行handleStartTag方法。function handleStartTag (match) { const tagName = match.tagName const unarySlash = match.unarySlash const unary = isUnaryTag(tagName) || !!unarySlash const l = match.attrs.length const attrs = new Array(l) for (let i = 0; i < l; i++) { const args = match.attrs[i] //345匹配三种属性值的不同写法 3: "tpl" 双引号,4: 'tpl' 单引号,5: tpl 没有引号 const value = args[3] || args[4] || args[5] || '' attrs[i] = { //将每一个属性转换成name和value这种结构的对象 name: args[1], value: decodeAttr(value, shouldDecodeNewlines) } } if (!unary) { //如果不是自闭合标签,push到stack stack.push({ tag: tagName, lowerCasedTag: tagName.toLowerCase(), attrs: attrs, start: match.start, end: match.end }) lastTag = tagName } if (options.start) { //执行options传入的开始标签回调 options.start(tagName, attrs, unary, match.start, match.end) } }
解析结束标签
const endTagMatch = html.match(endTag)
//如果endTag是,endTagMatch就是["</p>","p",index:0,...]
if (endTagMatch) {
const curIndex = index
advance(endTagMatch[0].length)
parseEndTag(endTagMatch[1], curIndex, index)
continue
}
结束标签的解析比开始标签简单许多,得到匹配结果后,执行parseEndTag
方法。
parseEndTag
方法做了一件事,就是通过stack检查标签是否闭合。
在handleStartTag
方法中,如果标签不是自闭合标签,会将这个标签push到stack。parseEndTag
方法会比较结束标签名和栈顶标签名是不是相同,如果相同就是闭合标签,直接pop;如果不同,就往栈底方向继续找,直到相同的标签名或者栈底,依次提示非闭合的标签,并将非闭合的标签和闭合的标签(如果有)一起pop。
function parseEndTag (tagName, start, end) {
let pos, lowerCasedTagName
//从栈顶开始查找tagName相同的标签
if (tagName) {
lowerCasedTagName = tagName.toLowerCase()
for (pos = stack.length - 1; pos >= 0; pos--) {
if (stack[pos].lowerCasedTag === lowerCasedTagName) {
break
}
}
} else {
pos = 0
}
if (pos >= 0) {
for (let i = stack.length - 1; i >= pos; i--) {
warn()
if (options.end) { //执行回调
options.end(stack[i].tag, start, end)
}
}
//pop非闭合的标签和闭合的标签(如果有)
stack.length = pos
lastTag = pos && stack[pos - 1].tag
}
}
开始标签的回调函数
解析完开始标签后,会调用开始标签的回调start
方法。
if (options.start) {
options.start(tagName, attrs, unary, match.start, match.end)
}
这个函数要做的事:
- 根据传入的tagName, attrs, unary等会生成一个 AST 节点
- 处理AST节点上的属性,attrs
生成AST节点的方法
function createASTElement (tag,attrs,parent) {
//创建一个AST节点需要标签名、属性值、和它的父节点信息
//一个AST节点的基本属性有以下这些
return {
type: 1,
tag,
attrsList: attrs,
attrsMap: makeAttrsMap(attrs), //将属性转换成键值对形式
rawAttrsMap: {},
parent,
children: []
}
}
function makeAttrsMap (attrs) {
const map = {}
for (let i = 0, l = attrs.length; i < l; i++) {
map[attrs[i].name] = attrs[i].value
}
return map
}
start
方法的执行过程:
const stack = []
let root
let currentParent
start (tag, attrs, unary, start, end) {
// 生成AST节点
let element = createASTElement(tag, attrs, currentParent)
//处理v-for、v-if、v-once这些指令
processFor(element)
processIf(element)
processOnce(element)
//如果没有没有根节点,就把当前节点设为根节点
if (!root) {
root = element
}
//如果不是自闭合标签,就入栈,并将currentParent指向当前节点
if (!unary) {
currentParent = element
stack.push(element)
} else {
closeElement(element)
}
}
结束标签的回调函数
创建AST树是一个深度优先的过程,从子节点向父节点回溯的条件就是执行结束标签的回调函数,借助栈来完成。在开始标签的回调函数中,遇到非自闭合的标签就入栈,执行结束标签回调函数就出栈。
end (tag, start, end) {
const element = stack[stack.length - 1]
// pop stack
stack.length -= 1
currentParent = stack[stack.length - 1]
closeElement(element)
}
function closeElement(element){
...
currentParent.children.push(element)
...
}
文本的回调函数
chars (text, start, end) {
//如果文本节点的父节点为空,则不处理,比如template里没有标签,直接是文本的情况
if (!currentParent) {
return
}
const children = currentParent.children
if (inPre || text.trim()) {
//父节点是否是script或style
text = isTextTag(currentParent) ? text : decodeHTMLCached(text)
}
if (text) {
let res
let child
//parseText方法用于解析表达式,如果是普通字符串,就返回undefined,执行else if的逻辑
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)
}
}
},
parseText
方法的第二个参数delimiters, 就是用来匹配表达式文本的,默认值也就是我们常用的双括号{{x}}
。
export function parseText (text,delimiters){
const tagRE = delimiters ? buildRegex(delimiters) : defaultTagRE
if (!tagRE.test(text)) { //没有匹配到{{x}},普通文本,就返回
return
}
const tokens = []
const rawTokens = []
let lastIndex = tagRE.lastIndex = 0
let match, index, tokenValue
while ((match = tagRE.exec(text))) {
index = match.index
// 提取表达式开始前的普通文本字符串
if (index > lastIndex) {
rawTokens.push(tokenValue = text.slice(lastIndex, index))
tokens.push(JSON.stringify(tokenValue))
}
// 提取表达式文本的内容
const exp = parseFilters(match[1].trim())
tokens.push(`_s(${exp})`)
rawTokens.push({ '@binding': exp })
lastIndex = index + match[0].length
}
// 提取表达式后的普通文本字符串
if (lastIndex < text.length) {
rawTokens.push(tokenValue = text.slice(lastIndex))
tokens.push(JSON.stringify(tokenValue))
}
return {
expression: tokens.join('+'),
tokens: rawTokens
}
}
这里最核心的方法就是exec
。参考MDN。
当正则表达式使用 "g" 标志时,可以多次执行 exec 方法来查找同一个字符串中的成功匹配。当你这样做时,查找将从正则表达式的 lastIndex 属性指定的位置开始。
匹配这样的文本<p>text:{{a}}, {{b}} and so on</p>
,得到的结果:
优化AST
生成AST后,下一步就是对它做优化。因为Vue是响应式的,但模板中并不是所有数据都是响应式,所以在patch过程中可以跳过这些静态数据的处理。怎样确定哪些是需要跳过的静态节点,就是优化optimize
要做的事情。
export function optimize (root, options) {
if (!root) return
//标记AST树中的静态节点
markStatic(root)
//标记静态根
markStaticRoots(root, false)
}
markStatic
就是调用isStatic
判断当前节点是否是静态节点,如果是就把static属性设置为 true, 否则设为false。如果是当前节点元素节点,就遍历所有子节点,同样调用markStatic
方法,这样递归下去,直到叶子节点。
function markStatic (node) {
node.static = isStatic(node)
if (node.type === 1) {
for (let i = 0, l = node.children.length; i < l; i++) {
const child = node.children[i]
markStatic(child)
//如果子节点中任何一个不是静态节点,那当前节点也不就是静态的
if (!child.static) {
node.static = false
}
}
}
}
function isStatic (node: ASTNode): boolean {
if (node.type === 2) { // expression
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)
))
}
markStaticRoots
的定义,它里面有一段注释,For a node to qualify as a static root, it should have children that are not just static text. Otherwise the cost of hoisting out will outweigh the benefits and it's better off to just always render it fresh.
意思大概就是说,如果一个元素节点,它只有一个普通文本节点,比如这样<div>abc</div>
,那么把它标记成静态节点的消耗还大一些,还不如不标。所以就新增了一个staticRoot属性,要是这种情况,staticRoot属性就为false。子节点的staticRoot属性不影响父节点的staticRoot属性。
function markStaticRoots (node: ASTNode, isInFor: boolean) {
if (node.type === 1) {
// For a node to qualify as a static root, it should have children that
// are not just static text. Otherwise the cost of hoisting out will
// outweigh the benefits and it's better off to just always render it fresh.
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 (node.ifConditions) {
for (let i = 1, l = node.ifConditions.length; i < l; i++) {
markStaticRoots(node.ifConditions[i].block, isInFor)
}
}
}
}
AST转换成render函数
function generate (ast,options) {
const state = new CodegenState(options)
const code = ast ? genElement(ast, state) : '_c("div")'
return {
render: `with(this){return ${code}}`,
staticRenderFns: state.staticRenderFns
}
}
generate
函数生成的是render字符串。后面会调用new Function(renderFuncString)
生成render
函数。
<div class="tpl"><p v-for="item in list">{{item}}</p><p>test</p></div>
with(this){
return _c('div',{
staticClass:"tpl"
},[
_l((list),function(item){
return _c('p',[_v(_s(item))])
}),
_c('p',[_v("test")])
],2)
}
with(obj)
将后面的{}中的语句块中对obj属性的访问可以省略书写obj,不用每次都去写 obj.属性 的形式,而是直接使用属性名。
比如上面,_c也就是this._c, _l也就是this._l,等等。这里的this是vue实例。
_c, _l, _v都是函数的缩写。
//src/core/instance/render.js
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
//src/core/instance/render-helpers/index.js
function installRenderHelpers (target) {
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
}
_l是renderList,_v是createTextVNode。
const code = ast ? genElement(ast, state) : '_c("div")'
render字符串的主体是调用genElement
方法生成的。
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) {
return genOnce(el, state)
} else if (el.for && !el.forProcessed) {
return genFor(el, state)
} else if (el.if && !el.ifProcessed) {
return genIf(el, state)
} else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
return genChildren(el, state) || 'void 0'
} else if (el.tag === 'slot') {
return genSlot(el, state)
} else {
let code
if (el.component) {
code = genComponent(el.component, el, state)
} else {
let data
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
})`
}
return code
}
}
genElement
方法中,首先考虑有各种指令的情况,比如v-for,v-once,v-if,v-slot等等,如果有这些指令,就进入对应的生成方法。如果没有,就先调用genData(el, state)
生成data,也就是例子中的{staticClass:"tpl"},再调用genChildren
方法生成children。
function genChildren (el,state,checkSkip,altGenElement,altGenNode) {
var children = el.children;
if (children.length) {
...
var gen = altGenNode || genNode;
return ("[" + (children.map(function (c) { return gen(c, state); }).join(',')) + "]" + (normalizationType$1 ? ("," + normalizationType$1) : ''))
}
}
genChildren
遍历children数组,每个child节点调用genNode
方法来处理。
function genNode (node, state) {
if (node.type === 1) {
return genElement(node, state)
} else if (node.type === 3 && node.isComment) {
return genComment(node)
} else {
return genText(node)
}
}
genNode
方法判断节点类型,如果是元素节点,就又调用genElement,如果是注释节点,就调用genComment,否则,当文本节点处理调用genText。
从AST根节点到叶子节点,整个过程是递归调用genElement,直到元素节点没有子节点,或者子节点是文本节点或注释节点。处理每一个节点时,根据它们自身的属性,选择对应的运行时渲染函数缩写。
vue源码系列文章: