上一个blog讲了vue模版编译的parse部分,这篇主要讲一下优化和生成的源码
var createCompiler = createCompilerCreator(function baseCompile (
template,
options
) {
var ast = parse(template.trim(), options);
if (options.optimize !== false) {
optimize(ast, options);
}
var code = generate(ast, options);
return {
ast: ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
});
optimize
/**
* Goal of the optimizer: walk the generated template AST tree
* and detect sub-trees that are purely static, i.e. parts of
* the DOM that never needs to change.
*
* Once we detect these sub-trees, we can:
*
* 1. Hoist them into constants, so that we no longer need to
* create fresh nodes for them on each re-render;
* 2. Completely skip them in the patching process.
*/
optimize的目标:walk一遍parse生成的ast树, 检测出来所有静态(dom中永远都不需要改变)的子树,一旦检测出来:
- 将它们变成常量, 这样re-render的时候不需要再去为他们生成新的node
- 在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)
}
optimize 方法主要有markStatic和markStaticRoots 两个方法,分别看下两个方法做了什么
// markStatic 将递归循环整个树,根据每个node的情况标记static为当前node是否是静态node
function markStatic (node: ASTNode) {
node.static = isStatic(node)
if (node.type === 1) {
// do not make component slot content static. this avoids
// 1. components not able to mutate slot nodes
// 2. static slot content fails for hot-reloading
// if (
// !isPlatformReservedTag(node.tag) &&
// node.tag !== 'slot' &&
// node.attrsMap['inline-template'] == null
// ) {
// return
// }
// 递归循环node.children
for (let i = 0, l = node.children.length; i < l; i++) {
const child = node.children[i]
markStatic(child)
// 如果子node的不是静态node,那么父node的static重制为false
if (!child.static) {
node.static = false
}
}
// 如果有ifConditions数组,则循环ifConditions数组中的所有情况的node
if (node.ifConditions) {
for (let i = 1, l = node.ifConditions.length; i < l; i++) {
const block = node.ifConditions[i].block
markStatic(block)
// 如果node的ifConditions中有一个情况的node不是静态的,那么node肯定也不是静态的
if (!block.static) {
node.static = false
}
}
}
}
}
具体看下isStatic是怎么判断是不是静态node的
function isStatic (node: ASTNode): boolean {
// 如果是个表达式,那么肯定不是静态的
if (node.type === 2) { // expression
return false
}
// 如果只是个文本,那么是静态的
if (node.type === 3) { // text
return true
}
// 如果是pre, 也就是指定不需要表达式,则是静态的
// 如果同时满足以下条件,则是静态的
// 没有指令
// 没有if
// 没有for
// tag是不是内部tag slot,component
// 是html原生的tag标签
// 父node不是for的template
// 是不是node所有的key都是静态的key
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将会标记最终的。 markStaticRoots同样是递归整颗树标记staticRoot, staticRoot必须要满足
- 当前node是静态的
- 必须要有children
- children不能只有一个且是文本
否则的话,对它的优化成本将大于优化后带来的收益。
function markStaticRoots (node, isInFor) {
if (node.type === 1) {
if (node.static || node.once) {
node.staticInFor = isInFor;
}
// 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 (var i = 0, l = node.children.length; i < l; i++) {
markStaticRoots(node.children[i], isInFor || !!node.for);
}
}
// 循环所有ifConditions
if (node.ifConditions) {
for (var i$1 = 1, l$1 = node.ifConditions.length; i$1 < l$1; i$1++) {
markStaticRoots(node.ifConditions[i$1].block, isInFor);
}
}
}
}
generate
function generate (
ast,
options
) {
var state = new CodegenState(options);
var code = ast ? genElement(ast, state) : '_c("div")';
return {
render: ("with(this){return " + code + "}"),
staticRenderFns: state.staticRenderFns
}
}
简单来说generate会生成一个render函数,执行render函数就可以得到vnode虚拟节点,用作之后进行patch和更新的。
先观察一下这个render函数, 可以看到使用了with(this)
这个就相当于
with(this){
console.log(a);
}
// 上面就相当于
console.log(this.a);
with 有个很大的问题就是性能,具体原因可以百度或者参考js高级程序设计60页。 但是vue为什么用这个了, 直接看下小右自己的回答吧。 如何看待Vue.js 2.0 的模板编译使用了with(this)的语法?
然后我们继续看genElement里面是怎么生成的:
function genElement (el, state) {
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) {
// once
return genOnce(el, state)
} else if (el.for && !el.forProcessed) {
// for
return genFor(el, state)
} else if (el.if && !el.ifProcessed) {
// if
return genIf(el, state)
} else if (el.tag === 'template' && !el.slotTarget && !state.pre) {
// template
return genChildren(el, state) || 'void 0'
} else if (el.tag === 'slot') {
// slot
return genSlot(el, state)
} else {
// component or element
var code;
if (el.component) {
code = genComponent(el.component, el, state);
} else {
var data;
if (!el.plain || (el.pre && state.maybeComponent(el))) {
data = genData$2(el, state);
}
var children = el.inlineTemplate ? null : genChildren(el, state, true);
code = "_c('" + (el.tag) + "'" + (data ? ("," + data) : '') + (children ? ("," + children) : '') + ")";
}
// module transforms
for (var i = 0; i < state.transforms.length; i++) {
code = state.transforms[i](el, code);
}
return code
}
}
可以看到,针对于不同的节点分别有不同的生成逻辑,具体生成的逻辑有兴趣可以自己看。 总结一下,其实还是递归遍历了所有的ast然后,分别根据各个node生成一个生成vnode的方法,然后把所有的拼接起来,感觉有点说不明白,大家可以看下这个地址。 template-explorer 这个可以在线生成最终的render函数, 更直观。
比如
<div id="app">{{ msg }}</div>
function render() {
with(this) {
return _c('div', {
attrs: {
"id": "app"
}
}, [_v(_s(msg))])
}
}
再比如
<div :id="app"><div v-for="item in items">{{ item }}</div></div>
function render() {
with(this) {
return _c('div', {
attrs: {
"id": app
}
}, _l((items), function (item) {
return _c('div', [_v(_s(item))])
}), 0)
}
}
具体里面的_l啊, _v之类的,可以对应到具体代码中看, 只要作用就是生成对应vnode
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;
}
至此,从模版生成render函数的流程就走完了,总结一下。
- parse ==> 主要就是将模版所有的信息转变成ast树,每个dom的参数变量等都会挂在树的节点上
- optimize ==> 优化阶段,主要是将静态根节点, 标记出来的根节点会直接跳过patch更新阶段,jieyueshijian
- generate ==> 生成阶段,循环遍历已经优化好的ast树,拼接成最终需要执行获取vnode的方法字符串