Closed lihongxun945 closed 6 years ago
如果大家有上过编译原理课,其实会比较容易看懂 compiler 模块的代码。所谓编译就是把一种语言转换成另一种语言的过程。在我们这里就是把字符串模板转换成 render 函数的过程。一般来说一个编译器会包括三个部分:
compiler
render
在我们的例子中,我们需要做如下转换:
compiler 会经过上面说的三个步骤,完成这个过程,我画了一个图来表示这个过程:
在图中 baseCompile 会 接收一个 template 字符串,然后调用 parse 把它转换成 抽象语法树 AST,然后再调用 generate 把语法树转成代码。注意这时候的代码是一个字符串,最后通过 createCompileToFunctionFn 把代码字符串转换成一个函数。现在看不懂图没关系,我们下面一步步通过代码来讲解
baseCompile
template
parse
AST
generate
createCompileToFunctionFn
parse 函数会进行词法和语法分析,最终生成一棵抽象语法树,parse 函数会调用 parseHTML 进行词法分析,然后把分析的结果进行语法分析,最后整理成一棵树。parse 函数特别长,为了方便阅读,这里我省略大部分代码,只保留基本的结构做说明
parseHTML
/** * Convert HTML string to AST. */ export function parse ( template: string, options: CompilerOptions ): ASTElement | void { // 一些配置的处理 // 这个变量是比较重要的,通过这个栈暂存对 parseHTML 返回的结果 const stack = [] let root // 最终语法树的根节点 parseHTML(template, { // 一些配置 start (tag, attrs, unary) { let element: ASTElement = createASTElement(tag, attrs, currentParent) // 对if,for, once 等指令进行一些处理 // tree management if (!root) { // 第一个处理的元素,把它作为根节点 root = element checkRootConstraints(root) } else if (!stack.length) { } // currentParent 是当前节点的父节点,因此我们直接把当前节点放入 currentParent.children 就行了 if (currentParent && !element.forbidden) { // 省略 // 构建父子节点 currentParent.children.push(element) element.parent = currentParent } } // 根据情况移动currentParent指针,如果是孩子关系就移动,兄弟关系就不移动。 if (!unary) { currentParent = element stack.push(element) } else { closeElement(element) } }, // 匹配到结束标签的时候,比如</div>就进行出栈操作,并且移动指针 end () { // remove trailing whitespace const element = stack[stack.length - 1] const lastNode = element.children[element.children.length - 1] if (lastNode && lastNode.type === 3 && lastNode.text === ' ' && !inPre) { element.children.pop() } // pop stack stack.length -= 1 currentParent = stack[stack.length - 1] closeElement(element) }, // 省略 }) return root }
parse 函数会调用 parseHTML 进行解析,parseHTML 会遍历模板字符串,每当找到开始节点的时候就调用 parse 中的 start 创建一个 element 并入栈,同时会处理好父子关系。每当匹配到一个结束节点的时候,就调用 end 进行出栈操作。
start
element
end
其实这是用深度优先遍历(DFS)的方式来生成一棵树,在不使用递归的情况下就是通过 stack 来保存遍历路径上的节点。举个例子来说明:
stack
<div class="hello”><span>123</span><p>1111</p></div>
这段HTML其实有一个根节点,和两个子节点。
当 parseHTML 扫描到 <div class=“hello”> 的时候,因为是一个开始节点,因此会调用 options.start 来处理。此时会创建一个根节点出来,如下图所示。其中红色箭头是 currentParent 指针,蓝色方框是 stack 栈:
<div class=“hello”>
options.start
currentParent
然后继续扫描,会碰到 <span> 节点,因为也是开始节点,所以继续进行压栈和移动指针操作,此时会变成这样:
<span>
再往下扫描的时候,会碰到 </span> 节点,因为是结束节点,所以进行出栈操作,同时把指针移动到栈的最后一个元素上,也就是 <div> 元素,此时变成这样:
</span>
<div>
注意上图中,我们为什么知道 span 出栈后应该怎么移动指针,是因为我们在栈中记录了。
span
接下来会碰到 <p> 节点,因为是开始节点,所以会创建一个新的元素,并入栈,同时移动指针:
<p>
然后继续扫描碰到 </p> 节点,进行出栈操作,同时移动指针:
</p>
最后,碰到 </div> 再次出栈,此时 stack 为空,说明已经解析完毕:
</div>
以上就是 parse 函数创建AST的过程,这里仅仅说明了如何创建一颗树,其实在每一个节点的创建的时候,都有很多情况要处理,比如节点类型可能是 slot 或者 template,节点上会有 attributes等需要取出来。这些我就不很细致的讲解了,有兴趣的话可以自行参阅源码。
slot
attributes
最终生成的AST如下所示:
在 parse 生成 ast 之后,我们就可以通过这个AST来生成目标代码了。codegen 是一个有限自动机DFA,他会从一个状态开始,根据条件向下一个状态转移。对于我们上文中的例子来说,其实逻辑比较简单,如下图所示:
ast
codegen
从genElement 入口开始处理根节点,在这个函数内部,会调用 genData 来生成 createElement函数需要用到的 data,这个data包含元素属性上的各种 attributes,我们在模板中可以定义的 class , style, directives 等都会被包含在data中,官方对data 的解释在这里:https://cn.vuejs.org/v2/guide/render-function.html#%E6%B7%B1%E5%85%A5-data-%E5%AF%B9%E8%B1%A1
genElement
genData
createElement
data
class
style
directives
genElement 还会对影响节点是否被渲染的一些特殊指令进行处理,比如 v-if, v-for, v-one 等。完整的代码如下:
v-if
v-for
v-one
export function genElement (el: ASTElement, state: CodegenState): string { if (el.staticRoot && !el.staticProcessed) { return genStatic(el, state) } else if (el.once && !el.onceProcessed) { return genOnce(el, state) } else if (el.for && !el.forProcessed) { return genFor(el, state) } else if (el.if && !el.ifProcessed) { return genIf(el, state) } else if (el.tag === 'template' && !el.slotTarget) { return genChildren(el, state) || 'void 0' } else if (el.tag === 'slot') { return genSlot(el, state) } else { // component or element let code if (el.component) { code = genComponent(el.component, el, state) } else { const data = el.plain ? undefined : genData(el, state) 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 } }
我们来看一下 genFor 是如何处理 for 循环的:
genFor
for
function genFor ( el, state, altGen, altHelper ) { var exp = el.for; var alias = el.alias; var iterator1 = el.iterator1 ? ("," + (el.iterator1)) : ''; var iterator2 = el.iterator2 ? ("," + (el.iterator2)) : ''; el.forProcessed = true; // avoid recursion return (altHelper || '_l') + "((" + exp + ")," + "function(" + alias + iterator1 + iterator2 + "){" + "return " + ((altGen || genElement)(el, state)) + '})' }
其中的三个参数 alias, iterator1 和 iterator2 分别是我们在下面这种用法时的三个形参:
alias
iterator1
iterator2
<div v-for="(value, key, index) in object"> {{ index }}. {{ key }}: {{ value }} </div>
最后生成代码也是直接通过拼接字符串实现的,如果我们有这样的模板:
<div class="hello"><p v-for="a in [1,2,3]">1111</p></div>
那么最终会生成这样的代码,其中 _l 是 renderList 他会遍历 我们传入的数组,并调用第二个参数进行渲染。
_l
renderList
"_l(([1,2,3]),function(a){return _c('p',[_v("1111")])})"
到这里我们弄懂了我们传入的 template 字符串,是如何被编译成render 函数的,其他的细节这里不再详细解读。下一章,我们讲如何 VDOM 的渲染。
图片暂时未上传,稍等一会
图片已上传:blush:
render函数生成的步骤概览
如果大家有上过编译原理课,其实会比较容易看懂
compiler
模块的代码。所谓编译就是把一种语言转换成另一种语言的过程。在我们这里就是把字符串模板转换成render
函数的过程。一般来说一个编译器会包括三个部分:在我们的例子中,我们需要做如下转换:
compiler 会经过上面说的三个步骤,完成这个过程,我画了一个图来表示这个过程:
在图中
baseCompile
会 接收一个template
字符串,然后调用parse
把它转换成 抽象语法树AST
,然后再调用generate
把语法树转成代码。注意这时候的代码是一个字符串,最后通过createCompileToFunctionFn
把代码字符串转换成一个函数。现在看不懂图没关系,我们下面一步步通过代码来讲解AST的生成:词法和语法分析
parse
函数会进行词法和语法分析,最终生成一棵抽象语法树,parse
函数会调用parseHTML
进行词法分析,然后把分析的结果进行语法分析,最后整理成一棵树。parse
函数特别长,为了方便阅读,这里我省略大部分代码,只保留基本的结构做说明parse
函数会调用parseHTML
进行解析,parseHTML
会遍历模板字符串,每当找到开始节点的时候就调用parse
中的start
创建一个element
并入栈,同时会处理好父子关系。每当匹配到一个结束节点的时候,就调用end
进行出栈操作。其实这是用深度优先遍历(DFS)的方式来生成一棵树,在不使用递归的情况下就是通过
stack
来保存遍历路径上的节点。举个例子来说明:这段HTML其实有一个根节点,和两个子节点。
当
parseHTML
扫描到<div class=“hello”>
的时候,因为是一个开始节点,因此会调用options.start
来处理。此时会创建一个根节点出来,如下图所示。其中红色箭头是currentParent
指针,蓝色方框是stack
栈:然后继续扫描,会碰到
<span>
节点,因为也是开始节点,所以继续进行压栈和移动指针操作,此时会变成这样:再往下扫描的时候,会碰到
</span>
节点,因为是结束节点,所以进行出栈操作,同时把指针移动到栈的最后一个元素上,也就是<div>
元素,此时变成这样:注意上图中,我们为什么知道
span
出栈后应该怎么移动指针,是因为我们在栈中记录了。接下来会碰到
<p>
节点,因为是开始节点,所以会创建一个新的元素,并入栈,同时移动指针:然后继续扫描碰到
</p>
节点,进行出栈操作,同时移动指针:最后,碰到
</div>
再次出栈,此时stack
为空,说明已经解析完毕:以上就是
parse
函数创建AST的过程,这里仅仅说明了如何创建一颗树,其实在每一个节点的创建的时候,都有很多情况要处理,比如节点类型可能是slot
或者template
,节点上会有attributes
等需要取出来。这些我就不很细致的讲解了,有兴趣的话可以自行参阅源码。最终生成的AST如下所示:
生成目标代码
在
parse
生成ast
之后,我们就可以通过这个AST来生成目标代码了。codegen
是一个有限自动机DFA,他会从一个状态开始,根据条件向下一个状态转移。对于我们上文中的例子来说,其实逻辑比较简单,如下图所示:从
genElement
入口开始处理根节点,在这个函数内部,会调用genData
来生成createElement
函数需要用到的data
,这个data包含元素属性上的各种attributes
,我们在模板中可以定义的class
,style
,directives
等都会被包含在data中,官方对data
的解释在这里:https://cn.vuejs.org/v2/guide/render-function.html#%E6%B7%B1%E5%85%A5-data-%E5%AF%B9%E8%B1%A1genElement
还会对影响节点是否被渲染的一些特殊指令进行处理,比如v-if
,v-for
,v-one
等。完整的代码如下:我们来看一下
genFor
是如何处理for
循环的:其中的三个参数
alias
,iterator1
和iterator2
分别是我们在下面这种用法时的三个形参:最后生成代码也是直接通过拼接字符串实现的,如果我们有这样的模板:
那么最终会生成这样的代码,其中
_l
是renderList
他会遍历 我们传入的数组,并调用第二个参数进行渲染。到这里我们弄懂了我们传入的
template
字符串,是如何被编译成render
函数的,其他的细节这里不再详细解读。下一章,我们讲如何 VDOM 的渲染。