在$mount
的时候,当遇到 Vue 实例传入的参数不包含 render,而是 template 或者 el 的时候,就会执行编译的过程,将另外两个转变为 render 函数。
在编译的过程中,有三个阶段:
本文只针对其中的 optimize 阶段进行重点阐述。
编译过程首先就是对模板做解析,生成 AST,它是一种抽象语法树,是对源代码的抽象语法结构的树状表现形式。在很多编译技术中,如 babel 编译 ES6 的代码都会先生成 AST。
生成的 AST 是一个树状结构,每一个节点都是一个 ast element
,除了它自身的一些属性,还维护了它的父子关系,如 parent
指向它的父节点,children
指向它的所有子节点。
parse
的目标是把 template
模板字符串转换成 AST 树,它是一种用 JavaScript 对象的形式来描述整个模板。那么整个 parse
的过程是利用正则表达式顺序解析模板,当解析到开始标签、闭合标签、文本的时候都会分别执行对应的回调函数,来达到构造 AST 树的目的。
AST 元素节点总共有 3 种类型:
过 parse
过程后,会输出生成 AST 树,那么接下来我们需要对这颗树做优化。为什么需要做优化呢?
因为 Vue 是数据驱动,是响应式的。但是我们的模板中,并不是所有的数据都是响应式的,也有很多的数据在首次渲染之后就永远不会变化了。既然如此,在我们执行 patch
的时候就可以跳过这些非响应式的比对。
简单来说:整个 optimize
的过程实际上就干 2 件事情,markStatic(root)
标记静态节点 ,markStaticRoots(root, false)
标记静态根节点。
/** * 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. */ 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) } 复制代码
通过代码来看,可以更好解析标记静态节点的逻辑:
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 } for (let i = 0, l = node.children.length; i < l; i++) { const child = node.children[i] markStatic(child) if (!child.static) { node.static = false } } if (node.ifConditions) { for (let i = 1, l = node.ifConditions.length; i < l; i++) { const block = node.ifConditions[i].block markStatic(block) if (!block.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) )) } 复制代码
代码解读:
isStatic()
中我们看到,isBuiltInTag
(即tag
为component
和slot
)的节点不会被标注为静态节点,isPlatformReservedTag
(即平台原生标签,web 端如 h1 、div标签等)也不会被标注为静态节点。children
,执行递归的markStatic
。node.ifConditions
表示的其实是包含有elseif
和 else
子节点,它们都不在children
中,因此对这些子节点也执行递归的markStatic
。static
的情况,那么父节点的static
属性就会变为 false。标记静态节点的作用是什么呢?其实是为了下面的标记静态根节点服务的。
function markStaticRoots (node: ASTNode, isInFor: boolean) { 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 (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) } } } } 复制代码
代码解读:
markStaticRoots()
方法针对的都是普通标签节点。表达式节点与纯文本节点都不在考虑范围内。markStatic()
得出的static
属性,在该方法中用上了。将每个节点都判断了一遍static
属性之后,就可以更快地确定静态根节点:通过判断对应节点是否是静态节点 且 内部有子元素 且 单一子节点的元素类型不是文本类型。注意:只有纯文本子节点时,他是静态节点,但不是静态根节点。静态根节点是 optimize 优化的条件,没有静态根节点,说明这部分不会被优化。
而 Vue 官方说明是,如果子节点只有一个纯文本节点,若进行优化,带来的成本就比好处多了。因此这种情况下,就不进行优化。
首先来分析一下,之所以在 optimize 过程中做这个静态根节点的优化,目的是什么,成本是什么?
目的:在 patch 过程中,减少不必要的比对过程,加速更新。
目的很好理解。那么成本呢?
成本:a. 需要维护静态模板的存储对象。b. 多层render函数调用.
详细解释这两个成本背后的细节:
一开始的时候,所有的静态根节点 都会被解析生成 VNode,并且被存在一个缓存对象中,就在 Vue.proto._staticTree 中。
随着静态根节点的增加,这个存储对象也会越来越大,那么占用的内存就会越来越多
势必要减少一些不必要的存储,所有只有纯文本的静态根节点就被排除了
这个过程涉及到实际操作更新的过程。在实际render 的过程中,针对静态节点的操作也需要调用对应的静态节点渲染函数,做一定的判断逻辑。这里需要一定的消耗。
如果纯文本节点不做优化,那么就是需要在更新的时候比对这部分纯文本节点咯?这么做的代价是什么呢?只是需要比对字符串是否相等而已。简直不要太简单,消耗简直不要太小。
既然如此,那么还需要维护多一个静态模板缓存么?在 render 操作过程中也不需要额外对该类型的静态节点进行处理。
staticRoot
属性会在我们编译过程的第三个阶段generate
阶段--生成 render 函数代码阶段--起到作用。generate
函数定义在src/compiler/codegen/index.js
中,我们详细来看:
export function generate ( ast: ASTElement | void, options: CompilerOptions ): CodegenResult { const state = new CodegenState(options) const code = ast ? genElement(ast, state) : '_c("div")' return { render: `with(this){return ${code}}`, staticRenderFns: state.staticRenderFns } } 复制代码
generate
函数首先通过 genElement(ast, state)
生成 code
,再把 code
用 with(this){return ${code}}}
包裹起来。这里的genElement
代码如下:
export function genElement (el: ASTElement, state: CodegenState): string { 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) { return genChildren(el, state) || 'void 0' } else if (el.tag === 'slot') { return genSlot(el, state) } else { // component or element let code if (el.component) { code = genComponent(el.component, el, state) } else { const data = el.plain ? undefined : genData(el, state) const children = el.inlineTemplate ? null : genChildren(el, state, true) code = `_c('${el.tag}'${ data ? `,${data}` : '' // data }${ children ? `,${children}` : '' // children })` } // module transforms for (let i = 0; i < state.transforms.length; i++) { code = state.transforms[i](el, code) } return code } } 复制代码
其中,首个判断条件就用到了节点的staticRoot
属性:
if (el.staticRoot && !el.staticProcessed) { return genStatic(el, state) } 复制代码
进入genStatic
:
// hoist static sub-trees out function genStatic (el: ASTElement, state: CodegenState): string { el.staticProcessed = true // Some elements (templates) need to behave differently inside of a v-pre // node. All pre nodes are static roots, so we can use this as a location to // wrap a state change and reset it upon exiting the pre node. const originalPreState = state.pre if (el.pre) { state.pre = el.pre } state.staticRenderFns.push(`with(this){return ${genElement(el, state)}}`) state.pre = originalPreState return `_m(${ state.staticRenderFns.length - 1 }${ el.staticInFor ? ',true' : '' })` } 复制代码
可以看到,genStatic
函数最终将对应的代码逻辑塞入到了state.staticRenderFns
中,并且返回了一个带有_m
函数的字符串,这个_m
是处理静态节点函数的缩写,为了方便生成的 render 函数字符串不要过于冗长。其具体的含义在src/core/instance/render-helpers/index.js
中:
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 } /** * Runtime helper for rendering static trees. */ export function renderStatic ( index: number, isInFor: boolean ): VNode | Array<VNode> { const cached = this._staticTrees || (this._staticTrees = []) let tree = cached[index] // if has already-rendered static tree and not inside v-for, // we can reuse the same tree. if (tree && !isInFor) { return tree } // otherwise, render a fresh tree. tree = cached[index] = this.$options.staticRenderFns[index].call( this._renderProxy, null, this // for render fns generated for functional component templates ) markStatic(tree, `__static__${index}`, false) return tree } 复制代码
在具体执行 render 函数的过程中,会执行_m
函数,其实执行的就是上面代码中的renderStatic
函数。静态节点的渲染逻辑是这样的:
_staticTrees
属性是否有对应的index
缓存值,若有,则直接使用。$options.staticRenderFns
对应的函数,结合genStatic
的代码,可知其对应执行的函数详细。在本文中,我们详细分析了 Vue 编译过程中的 optimize 过程。这个过程主要做了两个事情:标记静态节点markStatic
与标记静态根节点markStaticRoot
。同时,我们也分析了标记静态根节点markStaticRoot
在接下来的 generate 阶段的作用。
希望对读者有一定的帮助!若有理解不足之处,望指出!