本文我们一起通过学习Vue模板编译原理(二)-AST生成Render字符串来分析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 } }