Vue | 2021-03-10 02:05:13 2908次 2次
在 runtime 运行时,发现传入的是模板语法,则会走编译,编译后的字符串通过 new Function 形式传给组件的 render 方法。
vue 的 compiler 部分是一个转译器,和 babel 的原理上区别不大,都是用来做编译的,只不过 babel 处理的是 js,vue 处理的是一套自定义模板。
babel 是一个特殊的编译器(可以理解为转译器),它的大致工作原理为,简易编译器:
token -> parse -> travers -> transform -> generator
vue 的模板编译链路很短:
parse -> transform -> generator
Parse
vue 中处理 case 是很复杂的,尽管逻辑上很清晰,但是体量大了也很痛苦,所以在写 svu 过程中尽可能的简化过程,只保证一些流程调通,支持数据、事件、条件判断这三种情况,其他的暂时没有精力去写,所以编译部分的解析按照简化版的思路去分析。
这个过程中需要处理传入的模板字符串,解析为一颗 ast 语法树,这颗树描述了 dom 结构和对应的位置信息,比如如下一段模板:
template: ` <div> <h1 v-if="res.value" name = '11'> <p @click = "tes">11{{res.value}}</p> </h1> <h2 v-else> <p> <span>222</span> </p> </h2 > </div> `,
经过处理后的 ast 信息为:
就是将模板字符串转为一个 dom 结构,多了一个 loc 来记录字符串的位置信息,创造 ast 的时候就是做这么单纯的事情,还是比较容易理解的,接下来就看下这个处理的过程。
核心逻辑在 parseChildren,这个过程会递归处理,分为三个 case:
* 1、parseText 纯文本、字符串中的换行、空格等解析
* 2、parseElement 节点解析,这里会递归 parseChildren
* 3、parseInterpolation 动态绑定数据处理
parseElement 的过程是一个前序遍历时取出标签前半段,后序遍历修正,做到字符串的分割就是依据正则,大致过程如下:
function parseElement(context: ParserContext, ancestors: Element[]): any { // 拿出标签的前半部分 比如 <p @click = "tes"> const element = parseTag(context, TagType.Start); // children ancestors.push(element); // 递归执行 const children = parseChildren(context, ancestors); ancestors.pop(); element.children = children; // 消除后半段标签 if(startsWithEndTagOpen(context.source, element.tag)){ parseTag(context, TagType.End); } // 消除后半个标签 需要重新计算source,因为 offset 标志会后移 // 此时需要根据 context 最新的end位移来修正补全 source element.loc = getSelection(context, element.loc.start); return element; }
重点的计算在 parseTag,它主要根据一个正则来进行标签分段:
const tagRe = /^<\/?([a-z][^\t\r\n\f />]*)/i; function parseTag(context: ParserContext, type: TagType): any { const start = getCursor(context); const [tagStart, tag] = tagRe.exec(context.source)!; // <div 、 </div // 位置计算 advanceBy(context, tagStart.length); advanceSpace(context); // tag 空格 <div v-if=...> // 属性的解析 let props = parseAttributes(context, type); if(context.source.length){ // </div>: “</div”已被消除,这里消除“>” advanceBy(context, 1); } return{ type: NodeTypes.ELEMENT, tag, props, loc: getSelection(context, start) } }
所有的字符信息都是通过 context 中来操作,比如原始的一段字符串,经过前序编译过程中会取出标签前半段,后续遍历补充上后半段的闭合标签以及中间的内容部分(内容转换由 parseText 完成),通过一些图看下这个转换过程:
前序遍历
这个过程是从 div -> h1 -> p 前半部分【蓝框】先解析完成,存入对应节点上,在递归过程中发现【绿色】文本,则存入 p 的子节点,此时的 context 字符串信息只剩下【红框】部分,等待 h1 节点内所有子节点全部完成后(经过后续遍历),再进入 h2 解析。此时节点上的信息只有前半段,所以再经过后序遍历补全信息。
后序遍历
后序遍历时首先要处理的肯定是把上次遗留的标签处理干净:
.... </p> ..</ h>
消除完这些残留标签后,字符位置发生了改变,此时再重新根据最新的 context 偏移量和源字符串求出最终的字符串信息:
等到 h1 节点完成后,就轮到它的兄弟节点,h2 重复上述流程,整个节点的处理过程为:
div -- 进入
h1 -- 进入
p -- 进入
p -- 完成
h1 -- 完成
h2 -- 进入
p -- 进入
span -- 进入
span -- 完成
p -- 完成
h2 -- 完成
div -- 完成
流程上比较清晰了,明白了要解析一个 html 模板需要怎么做,那么既然这是一段字符串,如何进行分段?上面也提到了就是通过正则,只需要校验出 【<div】 或 【</div】 即可,然后标签上的属性再通过属性解析完成放入 props 中,最后手动剔除【>】即可,核心点就是 context 字符串的不断消减,这个过程中涉及到大量的位置计算调用,以及位置修正,这里不再展开描述。
Transform
拿到 ast 之后,ast 描述了这个模板节点的层级关系和其他信息,接下来要做一些转换操作,可以立即为语法制导分析,构造语法分析树,这样为了更方便的生成目标代码。
首先是从 traverseNode 开始进行的递归,svu 中只处理了如下几种语义:
1、模板 if:vIf
2、事件绑定:vOn
3、文本:transformText
4、节点:transformElement
至于静态节点处理是在转换完成后再进行处理,因为它需要知道某个节点有没有动态属性,没有的话就提升为静态节点,仅此而已,通过 patchFlag 是否有值进行判断是否为静态节点,这个值会在节点的处理过程中给出。
对于条件判断和事件绑定则是对 ast 树的前序遍历中进行信息处理,后序节点遍历中仅仅是生成固定的语法,保证数据的层层往上挂靠。对于文本、节点的处理都是在后序遍历过程中进行。
在做转换的过程中,会涉及到一些 ast 节点的操作,可以将一些操作方法进行类似 jq 式封装,方便节点的操作。
processIf
export function processIf(...) { // 属性解析时取到的 if (dir.name === 'if') { const branch = createIfBranch(node, dir) const ifNode = { type: NodeTypes.IF, loc: node.loc, branches: [branch] } // 旧的 ast 转为 v-if 信息 context.replaceNode(ifNode) if (processCodegen) { // 这里返回这个方法,等到v-if的节点完成时再执行 return processCodegen(ifNode, branch, true) } } else { // v-else // else 过程中肯定都是父节点的所有子节点 [if, else, else ...] const siblings = context.parent!.children let i = siblings.indexOf(node) while (i-- >= -1) { const sibling = siblings[i] if (sibling && sibling.type === NodeTypes.IF) { // 删除当前 else节点 为了和 if 的 branch放在一起 context.removeNode() const branch = createIfBranch(node, dir) // 推入 if 的branch sibling.branches.push(branch) const onExit = processCodegen && processCodegen(sibling, branch, false) // else 分支里面还有节点内容 需要再次进行转换 traverseNode(branch, context) // 这里和 if 不同,直接执行了,一样的道理 后序 onExit && onExit() context.currentNode = null } break } } }
再通过 processCodegen 回调,最终生成 if 分支(consequent)和 else 分支(alternate)的代码,结构如下图:
transformElement
vue 源码中,这部分写的内容很多,考虑了很多的情况,这里只考虑解析 props,比如事件的绑定解析,最终赋值到 vnode 的 dynamicProps,这样在runtime可以直接使用这个方法;再一个就是 patchFlag 的判断,判断是否为动态属性、静态属性,方便静态节点的提升。
Generator
得到转义后的信息,最后一步就是递归打印代码,最终通过 new Function 的形式执行。完结!
具体代码请查看:https://github.com/wineSu/svu
2人赞