在$mount
的时候,当遇到 Vue 实例传入的参数不包含 render,而是 template 或者 el 的时候,就会执行编译的过程,将另外两个转变为 render 函数。vue
在编译的过程当中,有三个阶段:node
本文只针对其中的 optimize 阶段进行重点阐述。web
编译过程首先就是对模板作解析,生成 AST,它是一种抽象语法树,是对源代码的抽象语法结构的树状表现形式。在不少编译技术中,如 babel 编译 ES6 的代码都会先生成 AST。正则表达式
生成的 AST 是一个树状结构,每个节点都是一个 ast element
,除了它自身的一些属性,还维护了它的父子关系,如 parent
指向它的父节点,children
指向它的全部子节点。算法
parse
的目标是把 template
模板字符串转换成 AST 树,它是一种用 JavaScript 对象的形式来描述整个模板。那么整个 parse
的过程是利用正则表达式顺序解析模板,当解析到开始标签、闭合标签、文本的时候都会分别执行对应的回调函数,来达到构造 AST 树的目的。express
AST 元素节点总共有 3 种类型:数组
过 parse
过程后,会输出生成 AST 树,那么接下来咱们须要对这颗树作优化。为何须要作优化呢?缓存
由于 Vue 是数据驱动,是响应式的。可是咱们的模板中,并非全部的数据都是响应式的,也有不少的数据在首次渲染以后就永远不会变化了。既然如此,在咱们执行 patch
的时候就能够跳过这些非响应式的比对。babel
简单来讲:整个 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 阶段的做用。
但愿对读者有必定的帮助!如有理解不足之处,望指出!
vue源码解读文章目录:
Vue 更多系列: