本文我们一起通过学习 vue 模板编译原理 (二)-AST 生成 Render 字符串来分析 Vue 源码. 预计接下来会围绕 Vue 源码来整理一些文章, 如下.
一起来学 Vue 双向绑定原理 - 数据劫持和发布订阅
一起来学 Vue 模板编译原理 (一)-Template 生成 AST
一起来学 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 的代码逻辑进行了说明.
相关
- https://github.com/lihongxun945/myblog/issues/29
- https://segmentfault.com/a/1190000012922342
来源: https://www.cnblogs.com/yzsunlei/p/12118360.html