前言
- 将组件的 html 模版解析成 AST 对象
- 基于 AST 语法树 进行静态标记,首先标记每个节点是否为 静态节点,然后进一步标记出静态 根节点,便于在后续更新中跳过静态根节点的更新,从而提高性能
下面就了解一下 vue 编译器是如何从 AST 语法树 生成运行渲染函数.
深入源码
createCompiler() 方法 —— 入口
文件位置:/src/compiler/index.js
其中最主要的就是 generate(ast, options)
方法,它负责从 AST
语法树生成渲染函数.
/* 在这之前做的所有的事情,只是为了构建平台特有的编译选项(options),比如 web 平台 1、将 html 模版解析成 ast 2、对 ast 树进行静态标记 3、将 ast 生成渲染函数 - 静态渲染函数放到 code.staticRenderFns 数组中 - 动态渲染函数 code.render - 在将来渲染时执行渲染函数能够得到 vnode */ export const createCompiler = createCompilerCreator(function baseCompile( template: string, options: CompilerOptions ): CompiledResult { /* 将模版字符串解析为 AST 语法树 每个节点的 ast 对象上都设置了元素的所有信息,如,标签信息、属性信息、插槽信息、父节点、子节点等 */ const ast = parse(template.trim(), options) /* 优化,遍历 AST,为每个节点做静态标记 - 标记每个节点是否为静态节点,保证在后续更新中跳过这些静态节点 - 标记出静态根节点,用于生成渲染函数阶段,生成静态根节点的渲染函数 优化,遍历 AST,为每个节点做静态标记 */ if (options.optimize !== false) { optimize(ast, options) } /* 从 AST 语法树生成渲染函数 如:code.render = "_c('div',{attrs:{"id":"app"}},_l((arr),function(item){return _c('div',{key:item},[_v(_s(item))])}),0)" */ const code = generate(ast, options) return { ast, render: code.render, staticRenderFns: code.staticRenderFns } }) 复制代码
generate() 方法
文件位置:src\compiler\codegen\index.js
其中在给 code
赋值时,主要的内容是通过 genElement(ast, state)
方法进行生成的.
/* 从 AST 生成渲染函数: - render 为字符串的代码 - staticRenderFns 为包含多个字符串的代码,形式为 `with(this){return xxx}` */ export function generate ( ast: ASTElement | void, // ast 对象 options: CompilerOptions // 编译选项 ): CodegenResult { /* 实例化 CodegenState 对象,参数是编译选项,最终得到 state ,其中大部分属性和 options 一样 */ const state = new CodegenState(options) /* 生成字符串格式的代码,比如:'_c(tag, data, children, normalizationType)' - data 为节点上的属性组成 JSON 字符串,比如 '{ key: xx, ref: xx, ... }' - children 为所有子节点的字符串格式的代码组成的字符串数组,格式: `['_c(tag, data, children)', ...],normalizationType`, - normalization 是 _c 的第四个参数,表示节点的规范化类型(非重点,可跳过) 注意:code 并不一定就是 _c,也有可能是其它的,比如整个组件都是静态的,则结果就为 _m(0) */ const code = ast ? (ast.tag === 'script' ? 'null' : genElement(ast, state)) : '_c("div")' return { render: `with(this){return ${code}}`, staticRenderFns: state.staticRenderFns } } 复制代码
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) { /* 处理静态根节点,生成节点的渲染函数 1、将当前静态节点的渲染函数放到 staticRenderFns 数组中 2、返回一个可执行函数 _m(idx, true or '') */ return genStatic(el, state) } else if (el.once && !el.onceProcessed) { /* 处理带有 v-once 指令的节点,结果会有三种: 1、当前节点存在 v-if 指令,得到一个三元表达式,`condition ? render1 : render2` 2、当前节点是一个包含在 v-for 指令内部的静态节点,得到 `_o(_c(tag, data, children), number, key)` 3、当前节点就是一个单纯的 v-once 节点,得到 `_m(idx, true of '')` */ return genOnce(el, state) } else if (el.for && !el.forProcessed) { /* 处理节点上的 v-for 指令,得到: `_l(exp, function(alias, iterator1, iterator2){return _c(tag, data, children)})` */ return genFor(el, state) } else if (el.if && !el.ifProcessed) { /* 处理带有 v-if 指令的节点,最终得到一个三元表达式:`condition ? render1 : render2` */ return genIf(el, state) } else if (el.tag === 'template' && !el.slotTarget && !state.pre) { /* 当前节点是 template 标签也不是 插槽 和 带有 v-pre 指令的节点时走这里 生成所有子节点的渲染函数,返回一个数组,格式如: `[_c(tag, data, children, normalizationType), ...]` */ return genChildren(el, state) || 'void 0' } else if (el.tag === 'slot') { /* 生成插槽的渲染函数,得到: `_t(slotName, children, attrs, bind)` */ return genSlot(el, state) } else { /* component or element 处理 动态组件 和 普通元素(自定义组件、原生标签、平台保留标签,如 web 平台中的每个 html 标签) */ let code if (el.component) { /* 处理动态组件,生成动态组件的渲染函数,得到 `_c(compName, data, children)` */ code = genComponent(el.component, el, state) } else { // 处理普通元素(自定义组件、原生标签) let data if (!el.plain || (el.pre && state.maybeComponent(el))) { /* 非普通元素或者带有 v-pre 指令的组件走这里,处理节点的所有属性,返回一个 JSON 字符串, 比如: '{ key: xx, ref: xx, ... }' */ data = genData(el, state) } /* 处理子节点,得到所有子节点字符串格式的代码组成的数组,格式: `['_c(tag, data, children)', ...],normalizationType` 其中的 normalization 表示节点的规范化类型(非重点,可跳过) */ const children = el.inlineTemplate ? null : genChildren(el, state, true) /* 得到最终的字符串格式的代码,格式:_c(tag, data, children, normalizationType) */ code = `_c('${el.tag}'${ data ? `,${data}` : '' // data }${ children ? `,${children}` : '' // children })` } /* 如果提供了 transformCode 方法,则最终的 code 会经过各个模块(module)的该方法处理, 不过框架没提供这个方法,不过即使处理了,最终的格式也是 _c(tag, data, children) module transforms */ for (let i = 0; i < state.transforms.length; i++) { code = state.transforms[i](el, code) } // 返回 code return code } } 复制代码
genChildren() 方法
文件位置:src\compiler\codegen\index.js
/* 生成所有子节点的渲染函数,返回一个数组,格式如: `[_c(tag, data, children, normalizationType), ...]` */ export function genChildren ( el: ASTElement, state: CodegenState, checkSkip?: boolean, altGenElement?: Function, altGenNode?: Function ): string | void { // 获取所有子节点 const children = el.children if (children.length) { // 第一个子节点 const el: any = children[0] // optimize single v-for if (children.length === 1 && el.for && el.tag !== 'template' && el.tag !== 'slot' ) { /* 优化处理: - 条件:只有一个子节点 && 子节点的上有 v-for 指令 && 子节点的标签不为 template 或者 slot - 方式:直接调用 genElement 生成该节点的渲染函数,不需要走下面的循环然后调用 genCode 最后得到渲染函数 */ const normalizationType = checkSkip ? state.maybeComponent(el) ? `,1` : `,0` : `` return `${(altGenElement || genElement)(el, state)}${normalizationType}` } // 获取节点规范化类型,返回一个 number: 0、1、2(非重点,可跳过) const normalizationType = checkSkip ? getNormalizationType(children, state.maybeComponent) : 0 // 是一个函数,负责生成代码的一个函数 const gen = altGenNode || genNode /* 返回一个数组,其中每个元素都是一个子节点的渲染函数 格式:['_c(tag, data, children, normalizationType)', ...] */ return `[${children.map(c => gen(c, state)).join(',')}]${ normalizationType ? `,${normalizationType}` : '' }` } } 复制代码
genNode() 方法
文件位置:src\compiler\codegen\index.js
function genNode (node: ASTNode, state: CodegenState): string { // 处理普通元素节点 if (node.type === 1) { return genElement(node, state) } else if (node.type === 3 && node.isComment) { // 处理文本注释节点 return genComment(node) } else { // 处理文本节点 return genText(node) } } 复制代码
genComment() 方法
文件位置:src\compiler\codegen\index.js
// 得到返回值,格式为:`_e(xxxx)` export function genComment (comment: ASTText): string { return `_e(${JSON.stringify(comment.text)})` } 复制代码
genText() 方法
文件位置:src\compiler\codegen\index.js
// 得到返回值,格式为:`_v(xxxxx)` export function genText (text: ASTText | ASTExpression): string { return `_v(${text.type === 2 ? text.expression // no need for () because already wrapped in _s() : transformSpecialNewlines(JSON.stringify(text.text)) })` }