「这是我参与2022首次更文挑战的第9天,活动详情查看:2022首次更文挑战」
前言
上篇文章分析了编译的parse部分,今天接着分析剩下的部分。主要是分为optimizer和generate。
optimizer
我们先分析optimize,optimize的作用在于标记静态节点。什么意思呢?
在我们编写template模板的时候,可以使用指令如v-if,动态属性如:name,或者动态文本{{}}。我们知道当数据改变时会重新渲染,实际上是会重新执行整个template函数。
所以编译的时候做了一个优化,在optimizer会标记静态节点,也就是没有和数据有绑定的节点,在第一次渲染时就保存渲染结果,再次执行整个template时可以不用重复执行这一部分,而是使用上次渲染结果就行。
我们来看看源码部分
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) {
// 1
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
}
// 2
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
}
// 提取node数据进行判断
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)
))
}
-
根据node数据判断是否静态节点且标记
-
遍历子节点标记静态节点,当子节点有非静态节点时说明父节点也是非静态节点,所以会更新父节点为非静态节点。
第二步则是标记静态节点的根节点,什么意思呢?加入有A->B两个节点,AB都是静态节点,A是B的父节点,则会标记A为staticRoot,这边就不做进一步分析了,实现过程大同小异。
generate
经过optimize后,得到的AST就可以用于生成渲染函数render了。我们来看看函数的实现generate
const code = generate(ast, options)
export function generate (
ast: ASTElement | void,
options: CompilerOptions
): CodegenResult {
const state = new CodegenState(options)
// fix #11483, Root level <script> tags should not be rendered.
const code = ast ? (ast.tag === 'script' ? 'null' : genElement(ast, state)) : '_c("div")'
return {
render: `with(this){return ${code}}`,
staticRenderFns: state.staticRenderFns
}
}
generate其实就是将ast通过递归的方式生成函数的代码字符串,后面再通过new Function()的方式执行。
在生成代码的过程中会使用许多辅助函数,如_c等,他们是在渲染模板中定义的,在这边将其拼接到代码串中,在执行的时候就可以调用相应的函数。我们可以看看他们的定义,从相关的名字就可以看出个大概
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
}
我们接着看看genElement的实现
export function genElement (el: ASTElement, state: CodegenState): string {
if (el.parent) {
el.pre = el.pre || el.parent.pre
}
// 1
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 {
// component or element
let code
if (el.component) {
code = genComponent(el.component, el, state)
} else {
// 2
let data
if (!el.plain || (el.pre && state.maybeComponent(el))) {
data = genData(el, state)
}
// 3
const children = el.inlineTemplate ? null : genChildren(el, state, true)
// 4
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)
}
// 5
return code
}
我们在上面将其分为5步进行分析
-
genStatic,genFor等。和我们之前在parse中分析的流程很想,就是根据节点中存在的不同指令属性等通过分类处理的方式实现不同处理。 -
genData根据el属性进行数据准备,实际生成个节点对象,也就是我们在渲染中分析的data -
通过
genChildren遍历子节点实现递归genElement -
将前面生成的节点数据
data,和子节点children拼接生成本节点解析的字符串,实际将作为_c的参数。_c其实就是render函数的第一个参数createElement,我们之前在渲染中有分析过。 -
返回前面拼接好的
code
上面的过程我们挑选几个关键实现来分析,如genStatic genFor genData
genStatic
function genStatic (el: ASTElement, state: CodegenState): string {
el.staticProcessed = true
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实际就是利用我们上一步optimize得到的静态根,通过其标识静态节点,在静态节点中将generate独立出来,这样就可以在后面单独缓存并使用了。
genFor
我们再看个指令的例子
export function genFor (
el: any,
state: CodegenState,
altGen?: Function,
altHelper?: string
): string {
const exp = el.for
const alias = el.alias
const iterator1 = el.iterator1 ? `,${el.iterator1}` : ''
// ...
el.forProcessed = true // avoid recursion
return `${altHelper || '_l'}((${exp}),` +
`function(${alias}${iterator1}${iterator2}){` +
`return ${(altGen || genElement)(el, state)}` +
'})'
}
可以看到,遇到指令的时候就会拼接_l及以指令表达式及变量为参数的函数,我们这边不去深究在渲染时候此段代码的实际运行,我们只需要知道对于不同指令的处理,其实就是拼接不同的函数或者代码就行。
我们在上面可以看到重复调用genElement,因为我们在genElement那边会将处理过的指令或者属性标记,所以不会重复调用同一逻辑导致暴栈,而是会走genElement的后面逻辑。
genData
genData就是通过节点属性来生成节点数据,就是我们后面渲染中的data数据。我们知道其是个对象,所以我们这边也要拼接个对象字符串才对。
函数的代码比较长,这边就不贴了。实际大多都是if的判断逻辑,我们列举其中几个就好。
export function genData (el: ASTElement, state: CodegenState): string {
let data = '{'
// key
if (el.key) {
data += `key:${el.key},`
}
// ref
if (el.ref) {
data += `ref:${el.ref},`
}
// module data generation functions
for (let i = 0; i < state.dataGenFns.length; i++) {
data += state.dataGenFns[i](el)
}
// attributes
if (el.attrs) {
data += `attrs:${genProps(el.attrs)},`
}
// ...
if (el.dynamicAttrs) {
data = `_b(${data},"${el.tag}",${genProps(el.dynamicAttrs)})`
}
return data
}
实例
我们来看看编译前面的例子吧
<div>
<div v-if="isShow" @click="doSomething" :class="activeClass">text{{name}}{{value}}text</div>
<div v-for="item in 10"></div>
<div name="1"><p>33</p></div>
</div>
{
render: `with(this){return _c('div',[(isShow)?_c('div',{cla…ction(item){return _c('div')}),_v(" "),_m(0)],2)}`,
staticRenderFns: [`with(this){return _c('div',{attrs:{\"name\":\"1\"}},[_c('p',[_v(\"33\")])])}`]
}
总结
本篇文章分析了编译的后两步optimize及generate,大概就是梳理了下其实现逻辑及原理,很多细节如指令,属性等的实现我们没有去深入分析,这部分可能等后面结合具体渲染逻辑分析比较好,我们在此就了解其主要流程步骤及实现原理就行。