[Vue源码]一起来学Vue模板编译原理(二)-AST生成Render字符串
本文我们一起通过学习vue模板编译原理(二)-ast生成render字符串来分析vue源码。预计接下来会围绕vue源码来整理一些文章,如下。
- 一起来学vue双向绑定原理-数据劫持和发布订阅
- 一起来学vue模板编译原理(一)-template生成ast
- 一起来学vue模板编译原理(二)-ast生成render字符串
- 一起来学vue虚拟dom解析-virtual dom实现和dom-diff算法
这些文章统一放在我的git仓库:。觉得有用记得star收藏。
编译过程
模板编译是vue中比较核心的一部分。关于 vue 编译原理这块的整体逻辑主要分三个部分,也可以说是分三步,前后关系如下:
第一步:将模板字符串转换成element asts(解析器)
第二步:对 ast 进行静态节点标记,主要用来做虚拟dom的渲染优化(优化器)
第三步:使用element asts生成render函数代码字符串(代码生成器)
对应的vue源码如下,源码位置在src/compiler/index.js
export const createcompiler = createcompilercreator(function basecompile ( template: string, options: compileroptions ): compiledresult { // 1.parse,模板字符串 转换成 抽象语法树(ast) const ast = parse(template.trim(), options) // 2.optimize,对 ast 进行静态节点标记 if (options.optimize !== false) { optimize(ast, options) } // 3.generate,抽象语法树(ast) 生成 render函数代码字符串 const code = generate(ast, options) return { ast, render: code.render, staticrenderfns: code.staticrenderfns } })
这篇文档主要讲第三步使用element asts生成render函数代码字符串,对应的源码实现我们通常称之为代码生成器。
代码生成器运行过程
在分析代码生成器的原理前,我们先举例看下代码生成器的具体作用。
<div> <p>{{name}}</p> </div>
在上节"template生成ast"中,我们已经说了通过解析器会把上面模板解析成抽象语法树(ast),解析结果如下:
{ tag: "div" type: 1, staticroot: false, static: false, plain: true, parent: undefined, attrslist: [], attrsmap: {}, children: [ { tag: "p" type: 1, staticroot: false, static: false, plain: true, parent: {tag: "div", ...}, attrslist: [], attrsmap: {}, children: [{ type: 2, text: "{{name}}", static: false, expression: "_s(name)" }] } ] }
- 而在这一节我们会将ast转换成可以直接执行的javascript字符串,最终结果如下:
with(this) { return _c('div', [_c('p', [_v(_s(name))]), _v(" "), _m(0)]) }
现在看不懂不要紧。其实生成器的过程就是 generate 函数拿到解析好的 ast 对象,递归 ast 树,为不同的 ast 节点创建不同的内部调用方法,然后组合成可执行的javascript字符串,等待后面的调用。
内部调用方法
我们看上面示例生成的javascript字符串,会发现里面会有_v
、_c
、_s
这样的东西,这些其实就是vue内部定义的一些调用方法。
其中 _c
函数定义在 src/core/instance/render.js
中。
vm.$slots = resolveslots(options._renderchildren, rendercontext) vm.$scopedslots = emptyobject // 定义的_c函数是用来创建元素的 vm._c = (a, b, c, d) => createelement(vm, a, b, c, d, false) vm.$createelement = (a, b, c, d) => createelement(vm, a, b, c, d, true)
而其他 _s
、_v
是定义在 src/core/instance/render-helpers/index.js
中:
export function installrenderhelpers (target: any) { target._o = markonce target._n = tonumber target._s = tostring target._l = renderlist //生成列表vnode target._t = renderslot //生成解析slot节点 target._q = looseequal target._i = looseindexof target._m = renderstatic //生成静态元素 target._f = resolvefilter target._k = checkkeycodes target._b = bindobjectprops //绑定对象属性 target._v = createtextvnode //创建文本vnode target._e = createemptyvnode //创建空节点vnode target._u = resolvescopedslots target._g = bindobjectlisteners target._d = binddynamickeys target._p = prependmodifier }
以上都是会在生成的javascript字符串中用到的,像比较常用的 _c
执行 createelement
去创建 vnode
, _l
对应 renderlist
渲染列表;_v
对应 createtextvnode
创建文本 vnode
;_e
对于 createemptyvnode
创建空的 vnode
。
整体逻辑
代码生成器的入口函数是generate
,具体定义如下,源码位置在src/compiler/codegen/index.js
export function generate ( ast: astelement | void, options: compileroptions ): codegenresult { // 初始化一些options const state = new codegenstate(options) // 传入ast和options进行生成 const code = ast ? genelement(ast, state) : '_c("div")' //重点 return { // 最外层包一个 with(this) 之后返回 render: `with(this){return ${code}}`, // 这个数组中的函数与 vdom 中的 diff 算法优化相关 // 那些被标记为 staticroot 节点的 vnode 就会单独生成 staticrenderfns staticrenderfns: state.staticrenderfns } }
生成器入口函数就比较简单,先初始化一些配置options,然后传入ast进行生成,没有ast时直接生成一个空div,最后返回生成的javascript字符串和静态节点的渲染函数。这里会把静态节点区分出来,便于后面的vnode diff,即vue比较算法更新dom,现在先略过。
现在我们重点看看genelement函数,代码位置在src/compiler/codegen/index.js
export function genelement (el: astelement, state: codegenstate): string { 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) { return genonce(el, state) // v-once处理生成函数 } else if (el.for && !el.forprocessed) { return genfor(el, state) // v-for处理生成函数 } else if (el.if && !el.ifprocessed) { return genif(el, state) // v-if处理生成函数 } else if (el.tag === 'template' && !el.slottarget && !state.pre) { return genchildren(el, state) || 'void 0' // 子节点处理生成函数 } else if (el.tag === 'slot') { return genslot(el, state) // slot处理生成函数 } else { // component or element let code if (el.component) { code = gencomponent(el.component, el, state) // component组件处理生成函数 } else { let data if (!el.plain || (el.pre && state.maybecomponent(el))) { data = gendata(el, state) // attributes节点属性处理生成函数 } 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 } }
你会发现genelement函数里面有很多条件判断。这是因为vue里面的指令写法实在太多,像 v-if
、 v-for
、 v-slot
等,每种指令写法都分离出一个函数来单独处理,这样代码阅读起来也清晰明了。下面,我们重点来看看 genfor
和 gendata
的具体处理逻辑。
genfor
genfor 函数是用来处理 v-for 指令写法的,源码位置在src/compiler/codegen/index.js
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}` : '' const iterator2 = el.iterator2 ? `,${el.iterator2}` : '' if (process.env.node_env !== 'production' && state.maybecomponent(el) && el.tag !== 'slot' && el.tag !== 'template' && !el.key ) { state.warn( `<${el.tag} v-for="${alias} in ${exp}">: component lists rendered with ` + `v-for should have explicit keys. ` + `see https://vuejs.org/guide/list.html#key for more info.`, el.rawattrsmap['v-for'], true /* tip */ ) } el.forprocessed = true // avoid recursion return `${althelper || '_l'}((${exp}),` + `function(${alias}${iterator1}${iterator2}){` + `return ${(altgen || genelement)(el, state)}` + '})' }
genfor 的逻辑其实就是,首先 ast 元素节点中获取了和 for 相关的一些属性,然后返回了一个代码字符串。
gendata
gendata 函数是用来处理节点属性的,源码位置在src/compiler/codegen/index.js
export function gendata (el: astelement, state: codegenstate): string { let data = '{' // directives first. // directives may mutate the el's other properties before they are generated. const dirs = gendirectives(el, state) if (dirs) data += dirs + ',' // key if (el.key) { data += `key:${el.key},` } // ref if (el.ref) { data += `ref:${el.ref},` } if (el.refinfor) { data += `refinfor:true,` } // pre if (el.pre) { data += `pre:true,` } // record original tag name for components using "is" attribute if (el.component) { data += `tag:"${el.tag}",` } // 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)},` } // dom props if (el.props) { data += `domprops:${genprops(el.props)},` } // event handlers if (el.events) { data += `${genhandlers(el.events, false)},` } if (el.nativeevents) { data += `${genhandlers(el.nativeevents, true)},` } // slot target,only for non-scoped slots if (el.slottarget && !el.slotscope) { data += `slot:${el.slottarget},` } // scoped slots if (el.scopedslots) { data += `${genscopedslots(el, el.scopedslots, state)},` } // component v-model if (el.model) { data += `model:{value:${ el.model.value },callback:${ el.model.callback },expression:${ el.model.expression }},` } // inline-template if (el.inlinetemplate) { const inlinetemplate = geninlinetemplate(el, state) if (inlinetemplate) { data += `${inlinetemplate},` } } data = data.replace(/,$/, '') + '}' // v-bind dynamic argument wrap if (el.dynamicattrs) { data = `_b(${data},"${el.tag}",${genprops(el.dynamicattrs)})` } // v-bind data wrap if (el.wrapdata) { data = el.wrapdata(data) } // v-on data wrap if (el.wraplisteners) { data = el.wraplisteners(data) } return data }
总结一下
代码生成是模板编译的第三步,它完成了ast到render的转换,即将抽象语法树ast转换成可以直接执行的javascript字符串。
其中genelement的代码比较多,因为需要分别处理的情况非常多,这里只是对genfor和gendata的代码逻辑进行了说明。