We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
如果大家有上过编译原理课,其实会比较容易看懂 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 的渲染。
下一章:Vue2.x源码解析系列八:深入$mount内部理解组件挂载和更新原理
The text was updated successfully, but these errors were encountered:
No branches or pull requests
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 的渲染。下一章:Vue2.x源码解析系列八:深入$mount内部理解组件挂载和更新原理
The text was updated successfully, but these errors were encountered: