资讯专栏INFORMATION COLUMN

【Vue原理】Compile - 源码版 之 generate 节点拼接

macg0406 / 2495人阅读

摘要:还原的难度就在于变成模板了,因为其他的什么等是原封不动的哈哈,可是直接照抄最后鉴于本人能力有限,难免会有疏漏错误的地方,请大家多多包涵,如果有任何描述不当的地方,欢迎后台联系本人,有重谢

</>复制代码

  1. 写文章不容易,点个赞呗兄弟


    专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧
    研究基于 Vue版本 【2.5.17

如果你觉得排版难看,请点击 下面链接 或者 拉到 下面关注公众号也可以吧

【Vue原理】Compile - 源码版 之 generate 节点拼接

终于走到了 Vue 渲染三巨头的最后一步了,那就是 generate,反正文章已经写完了,干脆早点发完,反正这部分内容大家也不会马上看哈哈

或者先看白话版好了
Compile - 白话版

然后,generate的作用就是,解析 parse 生成的 ast 节点,拼接成字符串,而这个字符串,是可以被转化成函数执行的。函数执行后,会生成对应的 Vnode

Vnode 就是Vue 页面的基础,我们就可以根据他完成DOM 的创建和更新了

比如这样

</>复制代码

  1. ast
  2. {
  3. tag:"div",
  4. children:[],
  5. attrsList:[{
  6. name:111
  7. }]
  8. }
  9. 拼接成函数
  10. "_c("div", { attrs:{ name:111 } }, [])"
  11. 转成函数
  12. new Function(传入上面的字符串)
  13. 生成 Vnode
  14. {
  15. tag: "div",
  16. data:{
  17. attrs: {name: "111"}
  18. },
  19. children: undefined
  20. }
本文概览

本文主要讲的是如果去把 生成好的 ast 拼接成 函数字符串(跟上面那个转换一样),而 ast 分为很多种,而每一种的拼接方式都不一样,我们会针对每一种方式来详细列出来

下面将会讲这么多种类型节点的拼接

静态节点,v-for 节点,v-if 节点,slot 节点,组件节点,子节点 等的拼接,内容较多却不复杂,甚至有点有趣

那我们就来看看 generate 本身的函数源码先

比较简短

</>复制代码

  1. function generate(ast, options) {
  2. var state = new CodegenState(options);
  3. var code = ast ? genElement(ast, state) : "_c("div")";
  4. return {
  5. render: "with(this){ return " + code + "}",
  6. //专门用于存放静态根节点的
  7. staticRenderFns: state.staticRenderFns
  8. }
  9. }

对上面出现的几个可能有点迷惑的东西解释一下

参数 options

options 是传入的一些判断函数或者指令函数,如下,不一一列举

</>复制代码

  1. {
  2. expectHTML: true,
  3. modules: modules$1,
  4. directives: directives$1
  5. ....
  6. };
函数 CodegenState

给该实例初始化编译的状态,下面会有源码

函数 genElement

把 ast 转成字符串的 罪魁祸首

generate 返回值

你也看到了

1 返回 genElement 拼接后的字符串code

这就是作为 render 的主要形态,包了一层 with

render 会有一块内容专门说,with 就不多说了哈,就是为了为 render 绑定实例为上下文

2 返回 静态根节点 的 静态render

这是一个 数组,因为一个模板里面可能存在多个静态根节点,那么就要把这些静态根节点都转换成 render 字符串保存起来,就是保存在数组中

上面是静态根节点?简单就是说,第一静态,第二某一部分静态节点的最大祖宗,如下图

两个 span 就是 静态根节点,他们都是他们那个静态部分的最大祖宗,而 div 下 有 v-if 的子节点,所以 div 不是静态根节点

然后下面这个静态模板,解析得到 render 放到 staticRenderFns 是这样的

</>复制代码

  1. 111
  2. staticRenderFns=[
  3. `
  4. with(this){
  5. return _c("div",
  6. {attrs:{"name":"a"}},[111])]
  7. )
  8. }
  9. `
  10. ]

而 staticRenderFns 也会在 render 模块下详细记录

CodegenState

初始化实例的编译状态

</>复制代码

  1. function CodegenState(options) {
  2. this.options = options;
  3. this.dataGenFns = [ klass$1.genData, style$1.genData];
  4. this.directives = { on , bind, cloak, model,text ,html]
  5. this.staticRenderFns = [];
  6. };

因为这个函数是给实例初始化一些属性的,看到很明显就是给实例添加上了很多属性,this.xxxx 什么的,那么我们就对 CodegenState 这个函数中添加的属性解释一下。

属性 dataGenFns

这个数组,存放的是两个函数

style$1.genData 处理 ast 中的 style ,包括动态静态的 style

klass$1.genData 处理 ast 中的 class ,包括动态静态的 class

比如

</>复制代码

  1. 解析成 ast
  2. {
  3. tag: "div",
  4. type: 1,
  5. staticStyle: "{"height":"0"}",
  6. styleBinding: "{width:0}",
  7. staticClass: ""a"",
  8. classBinding: "name"
  9. }
  10. 解析成字符串
  11. `_c("div",{
  12. staticClass:"a",
  13. class:name,
  14. staticStyle:{"height":"0"},
  15. style:{width:0}
  16. })
  17. `
  18. staticClass:"a",
  19. class:name,
  20. staticStyle:{"height":"0"},
  21. style:{width:0}
  22. })
  23. `

dataGenFns 会在后面拼接节点数据的时候调用到

属性 directives

这也是个数组,存放的是 Vue 自有指令的独属处理函数

包括以下几个指令的处理函数

v-on,绑定事件

v-bind,绑定属性

v-cloak,编译前隐藏DOM

v-model,双向绑定

v-text,插入文本

v-html,插入html

当你在模板中使用到以上的指令的时候,Vue 会调用相应的函数先进行处理

属性 staticRenderFns

一个数组,用来存放静态根节点的render 函数,上面有提到过一点

每个实例都独有这个属性,如果没有静态根节点就为空

比如下面这个模板,有两个静态根节点

然后在实例 的 staticRenderFns 中就存放两个 静态 render

那么我们现在就来看,generate 的重点函数,genElement

genElement

这是 ast 拼接成 字符串 的重点函数,主要是处理各种节点,并且拼接起来

静态节点,v-for 节点,v-if 节点,slot 节点,组件节点,子节点 等,有一些省略了

可以简单看看下面的源码

</>复制代码

  1. function genElement(el, state) {
  2. if (
  3. el.staticRoot && !el.staticProcessed
  4. ) {
  5. return genStatic(el, state)
  6. }
  7. else if (
  8. el.for && !el.forProcessed
  9. ) {
  10. return genFor(el, state)
  11. }
  12. else if (
  13. el.if && !el.ifProcessed
  14. ) {
  15. return genIf(el, state)
  16. }
  17. else if (el.tag === "slot") {
  18. return genSlot(el, state)
  19. }
  20. else {
  21. var code;
  22. // 处理 is 绑定的组件
  23. if (el.component) {
  24. code = genComponent(el.component, el, state);
  25. }
  26. // 上面所有的解析完之后,会走到这一步
  27. else {
  28. // 当 el 不存在属性的时候,el.plain = true
  29. var data = el.plain ? undefined : genData$2(el, state);
  30. // 处理完父节点,遍历处理所有子节点
  31. var children = genChildren(el, state);
  32. code = `_c(
  33. "${el.tag}"
  34. ${data ? ("," + data) : ""}
  35. ${children ? ("," + children) : ""}
  36. )`
  37. }
  38. return code
  39. }
  40. }

重点是其中的各种处理函数,通过各种条件来选择函数进行处理,并且会有一个 xxxProcessed 属性,作用是证明是否已经进行过 xxx 方面的处理了,比如forProcessed = true,证明已经拼接过他的 v-for 了

在相应的函数中,会被这个属性设置为 true,然后递归的时候,就不会再调用相应的函数

以上的各种函数中会调用 genElement,以便递归处理其他节点

genElement 按顺序处理自身各种类型的节点后,开始 genData$2 拼接节点的数据,比如 attr ,prop 那些,然后再使用 genChildren 处理 子节点

拼接节点数据会在独立一篇文章记录,内容很多

下面我们来一个个看其中涉及的节点处理函数

拼接静态节点

</>复制代码

  1. function genStatic(el, state) {
  2. el.staticProcessed = true;
  3. state.staticRenderFns.push(
  4. "with(this){ return " + genElement(el, state) + "}"
  5. );
  6. return `
  7. _m(${
  8. state.staticRenderFns.length - 1
  9. })
  10. `
  11. }

太简单了,给一个模板看一下就可以了

处理完,存储静态render,并返回字符串 "_m(0)" , 很简短吼

意思就是获取 staticRenderFns 中的第一个值

其中的值,也是调用 genElement 得到的静态 render

拼接 vIf 节点

专门用于处理带有 v-if 的节点

</>复制代码

  1. function genIf(
  2. el, state) {
  3. el.ifProcessed = true; // 避免递归
  4. return genIfConditions(
  5. el.ifConditions.slice(),
  6. state
  7. )
  8. }

看到 parse 文章的,想必应该知道 el.ifCondition 是什么了吧

简单说一下吧,el.ifCondition 是用来存放条件节点的数组

什么叫条件节点啊?

比如 你有这样的模板

像 上面的 p,span,section 三个节点都是条件节点,不会直接存放到父节点的 children 中

因为并不是马上显示的

然后他们解析得到的 ast ,都会被存放到 p 节点的 ifCondition 中

像这样

</>复制代码

  1. {
  2. tag:"div",
  3. children:[{
  4. tag:"p",
  5. ifCondition:[{
  6. exp: "isShow",
  7. block: {..p 的 ast 节点}
  8. },{
  9. exp: "isShow==2",
  10. block: {..span 的 ast 节点}
  11. },{
  12. exp: undefined,
  13. block: {..section 的 ast 节点}
  14. }]
  15. }]
  16. }

el.ifCondition 就是把 这个数组复制一遍(我又学到了,之前我并不知道可以这么去复制数组)

然后传给 genIfCondition,看看源码

</>复制代码

  1. function genIfConditions(
  2. conditions, state,
  3. ) {
  4. // 当 没有条件的时候,就返回 _e ,意思是空节点
  5. if (!conditions.length) {
  6. return "_e()"
  7. }
  8. // 遍历一遍之后,就把条件剔除
  9. var condition = conditions.shift();
  10. if (condition.exp) {
  11. return (
  12. condition.exp + "?" +
  13. genElement(condition.block,state) + ":" +
  14. genIfConditions(conditions, state )
  15. )
  16. } else {
  17. return genElement(condition.block,state)
  18. }
  19. }

这个函数的作用呢,是这样的

1、按顺序处理 ifCondition 中的每一个节点,并且会移出数组

2、并且每一个节点使用 三元表达式 去拼接

3、递归调用 genIfConditions 去处理剩下的 ifCondition

按下面的模板来说明一下流程

第一轮

ifCondition = [ p,span,section ]

获取 ifCondition 第一个节点,也就是p,并移出 ifCondition 数组

此时 ifCondition = [ span,section ]

p 节点有表达式 isShow,需要三元表达式拼接,变成

</>复制代码

  1. " isShow ? _c("p") : genIfConditions( 剩下的 ifCondition )"
第二轮

genIfConditions 同样获取第一个节点,span

此时 ifCondition = [ section ]

span 有表达式 isShow==2,需要拼接三元表达式,变成

</>复制代码

  1. " isShow ?
  2. _c("p") :
  3. ( isShow==2 ? _c( "span") : genIfConditions( 剩下的 ifCondition ) )"
第三轮

genIfConditions 同样获取第一个节点,section

此时 ifCondition = [ ]

section 没有表达式,直接处理节点,拼接成

</>复制代码

  1. " isShow ?
  2. _c("p") :
  3. ( isShow==2 ? _c( "span") : _c( "section") )"

然后就处理完啦,上面的字符串,就是 genIf 处理后拼接上的字符串

接下来看是怎么拼接带有v-for 的指令的

拼接v-for 节点

</>复制代码

  1. function genFor(
  2. el, state
  3. ) {
  4. var exp = el.for;
  5. var alias = el.alias;
  6. var iterator1 = el.iterator1 ? ("," + el.iterator1 ) : "";
  7. var iterator2 = el.iterator2 ? ("," + el.iterator2 ) : "";
  8. el.forProcessed = true; // avoid recursion
  9. return (
  10. "_l (" + exp + ",function(" + alias + iterator1 + iterator2 + "){" +
  11. "return " + genElement(el, state) +
  12. "})"
  13. )
  14. }

大家应该都可以看得懂的吧,给个例子

</>复制代码

  1. `_c("div", _l( arr ,function(item,index){ return _c("span") } )`

就这样,v-for 就解析成了一个 _l 函数,这个函数会遍历 arr,遍历一遍,就生成一个节点

下面在看看是如何处理子节点的

拼接子节点

</>复制代码

  1. function genChildren(el, state) {
  2. var children = el.children;
  3. if (!children.length) return
  4. return` [$ {
  5. children.map(function(c) {
  6. if (node.type === 1) {
  7. return genElement(node, state)
  8. }
  9. return`_v($ {
  10. text.type === 2
  11. ? text.expression
  12. : (""" + text.text + """)
  13. })`
  14. }).join(","))
  15. }]`
  16. }

同样的,这个函数也是很简单的吼

就是遍历所有子节点,逐个处理子节点,然后得到一个新的数组

1、当子节点 type ==1 时,说明是标签,那么就要 genElement 处理一遍

2、否则,就是文本节点

如果 type =2 ,那么是表达式文本,否则,就是普通文本

普通文本,需要左右两边加引号。表达式是个变量,需要在实例上获取,所以不用加双引号

举个例子

解析成字符串

</>复制代码

  1. `_c("div",[ _c("span") ,_c("section") ,_c("a") ])`
拼接插槽

</>复制代码

  1. function genSlot(el, state) {
  2. var slotName = el.slotName || ""default"";
  3. var children = genChildren(el, state);
  4. var res = `
  5. _t( ${slotName} ,
  6. ${ children ? ("," + children) : ""}
  7. )
  8. `
  9. var attrs =
  10. el.attrs && "{" +
  11. el.attrs.map(function(a) {
  12. return camelize(a.name) + ":" + a.value;
  13. }).join(",") +
  14. "}";
  15. if (attrs && !children) {
  16. res += ",null";
  17. }
  18. // _t 的参数顺序是 slotName, children,attrs,bind
  19. if (attrs) {
  20. res += "," + attrs;
  21. }
  22. return res + ")"
  23. }

genSlot 主要是处理子节点 和 绑定在 slot 上的 attrs

属性 attrs 会逐个拼接成 xx:xx 的形式 ,合成一个新的数组,然后通过 逗号隔开成字符串

</>复制代码

  1. 原 attrs = [
  2. { name:"a-a" ,value:"aaa"},
  3. { name:"b-b" ,value:"bbb"}
  4. ]
  5. 转换后,name 会变成驼峰
  6. attrs = "aA:"aaa", bB:"bbb""

看下例子,一个slot,绑定属性 a 作为 scope,并且有 span 作为默认内容

解析成这样

</>复制代码

  1. _c("div",[_t("default", [_c("span")] ,{a:aa} )] )

然后剩最后一个了,解析组件的节点

拼接组件

</>复制代码

  1. function genComponent(componentName, el, state) {
  2. var children = genChildren(el, state, true);
  3. return `_c(
  4. ${componentName},
  5. ${genData$2(el, state)}
  6. ${children ? ("," + children) : ""}
  7. )`
  8. }

其实,解析组件,就是把他先当做普通标签进行处理,在这里并没有做什么特殊的操作

但是这个方法是用来处理 【带有 is 属性】 的节点

否则 就不会存在 el.component 这个属性,就不会调用 genComponent

拼接成下面这样,而其中的 is 属性的拼接在 下篇文章 genData$2 中会有说明

</>复制代码

  1. `_c("div",[_c("test",{tag:"a"})])`

那如果直接写组件名作为标签,是怎么处理?

也没有做什么特殊处理,具体看 genElement 最后那段

同样当做普通标签先解析

看个例子

解析成这样的字符串

</>复制代码

  1. `_c("div",[ _c("test", [_c("span")] )] )`
走个流程

看了上面这么多的处理函数,各种函数处理后得到的字符串是相加的关系

然后现在用一个小例子来实现以下拼接步骤

1、先解析最外层 div,得到字符串

</>复制代码

  1. `_c( "div" `

genChildren 开始解析子节点

2、处理 strong,这是一个静态根节点,genStatic 处理得到字符串

</>复制代码

  1. `_c( "div" , [ _m(0) `

3、处理 p 节点,genIf 处理拼接字符串

</>复制代码

  1. `_c( "div" , [ _m(0) , isShow? _c(p) :_e() `

4、处理 span 节点, genFor 拼接字符串

</>复制代码

  1. `_c( "div" , [
  2. _m(0) ,
  3. isShow? _c(p) :_e() ,
  4. _l(arr,function(item,index){return _c("span")})
  5. `

5、处理 test 组件节点,genComponent 拼接

</>复制代码

  1. `_c( "div" , [
  2. _m(0) ,
  3. isShow? _c(p) :_e() ,
  4. _l(arr,function(item,index){return _c("span")}),
  5. _c("test")
  6. `

6、genChildren 处理完所有子节点拼接上末尾的括号得到

</>复制代码

  1. `_c( "div" , [
  2. _m(0) ,
  3. isShow? _c(p) :_e() ,
  4. _l(arr,function(item,index){return _c("span")}),
  5. _c("test")
  6. ])
  7. `

然后整个genElement 流程就处理完了

上面得到的 字符串,只要转换成函数,就可以执行了,于是也就可以得到我们的 Vnode

感悟

有时你会想,看这个东西有什么用啊,其实你只做正常项目的话,你的确大可不必去看这部分的内容,但是如果你真的想胸有成竹,百分百掌握Vue,你就必须看,因为你可以做更多东西

比如之前接了个外包,要根据别人打包好的文件,去还原别人的源码!

难度之大之复杂,你也想得出来,不过幸好我看过源码,打包后的文件,模板全是 render 函数,所以我可以手动还原出来原始模板!

虽然我也可以写一个 反编译模板函数,但是工作量太大,没得想法了。还原的难度就在于 render 变成模板了,因为其他的什么 method 等是原封不动的哈哈,可是直接照抄

最后

鉴于本人能力有限,难免会有疏漏错误的地方,请大家多多包涵,如果有任何描述不当的地方,欢迎后台联系本人,有重谢

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/106698.html

相关文章

  • Vue原理Compile - 源码 generate 节点数据拼接

    摘要:写文章不容易,点个赞呗兄弟专注源码分享,文章分为白话版和源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧研究基于版本如果你觉得排版难看,请点击下面链接或者拉到下面关注公众号也可以吧原理源码版之节点数据拼接上一篇我们 写文章不容易,点个赞呗兄弟 专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧研究...

    fizz 评论0 收藏0
  • Vue原理Compile - 白话

    摘要:写文章不容易,点个赞呗兄弟专注源码分享,文章分为白话版和源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧研究基于版本如果你觉得排版难看,请点击下面链接或者拉到下面关注公众号也可以吧原理白话版终于到了要讲白话的时候了 写文章不容易,点个赞呗兄弟 专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧研究...

    dingding199389 评论0 收藏0
  • Vue原理Compile - 源码 generate 拼接绑定的事件

    摘要:写文章不容易,点个赞呗兄弟专注源码分享,文章分为白话版和源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧研究基于版本如果你觉得排版难看,请点击下面链接或者拉到下面关注公众号也可以吧原理源码版之拼接绑定的事件今天我们 写文章不容易,点个赞呗兄弟 专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧研究...

    OnlyMyRailgun 评论0 收藏0
  • Vue原理Compile - 源码 optimize 标记静态节点

    摘要:一旦我们检测到这些子树,我们可以把它们变成常数,这样我们就不需要了在每次重新渲染时为它们创建新的节点在修补过程中完全跳过它们。否则,吊装费用将会增加好处大于好处,最好总是保持新鲜。 写文章不容易,点个赞呗兄弟 专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧研究基于 Vue版本 【2.5.17】 如果你觉得排版难看,...

    Soarkey 评论0 收藏0
  • Vue原理Compile - 源码 从新建实例到 compile结束的主要流程

    摘要:页面这个实例,按理就需要解析两次,但是有缓存之后就不会理清思路也就是说,其实内核就是不过是经过了两波包装的第一波包装在中的内部函数中内部函数的作用是合并公共和自定义,但是相关代码已经省略,另一个就是执行第二波包装在中,目的是进行缓存 写文章不容易,点个赞呗兄弟 专注 Vue 源码分享,文章分为白话版和 源码版,白话版助于理解工作原理,源码版助于了解内部详情,让我们一起学习吧研究基于 ...

    CODING 评论0 收藏0

发表评论

0条评论

macg0406

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<