「这是我参与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
,大概就是梳理了下其实现逻辑及原理,很多细节如指令,属性等的实现我们没有去深入分析,这部分可能等后面结合具体渲染逻辑分析比较好,我们在此就了解其主要流程步骤及实现原理就行。