RISC- Compiler Reordering- template-compiler分析

RISC- Compiler Reordering- template-compiler分析

参考文献链接

https://mp.weixin.qq.com/s/0O7BFr6Jh1JTuK7d-icRsA

https://mp.weixin.qq.com/s/ZLS317bjT3teuzE_ELpgGA

https://mp.weixin.qq.com/s/mh2P34tdkRaYMZ9I9yoztA

精简指令集集(RISC)处理器

一般来说,人类历史第一台RISC 电脑,应当是CDC6600,姑且不论IBM 力图自我宣传的立场,IBM 801 研发过程逐步厘清RISC 该有的样貌,也是不争的事实。最起码IBM 801 研发团队明确指出「存储器载入/回存(Load / Store) 架构」与「微码不好(Microcode is bad),不要微码」这些历史性结论。

 

 创造「根据代码流可分成指令(Instruction) 和数据(Data) 两种、据此再分成四种计算机类型(SISD、SIMD、MISD、MIMD)」的「费林分类法」(Flynn′s Taxonomy)的Michael Flynn,认定IBM 801 是世界第一个RISC 系统。IBM 前述ACS 计划累积的成果,如高级语言编译器的最佳化手段和更精练的指令集架构,一并延续到IBM 801。

 

 

 ▲ 第一台RISC计算机IBM 801,由IBM 研究员John Cocke 和团队于1970 年代后期设计,他也在1987 年获得计算机工业最高荣誉「图灵奖」。1974 年,IBM 开始研究如何打造每小时处理100 万则通话、平均每秒300 通的电话交换机。假如每通电话须执行2万个指令,代表每秒最少要600 万个指令(6MIPS),再算额外处理负担,起码要1,200 万个指令(12MIPS)。但当时IBM 销售最高端机种System/370 Model 168(1972 年),只有每秒300 万个指令(3MIPS),这说明要出现巨大技术突破,才有实现目标的可能。因此John Cocke 团队删除所有操作存储器内数据的指令,只保留使用处理器内的数据寄存器。相较未有专门指令负责存储器载入(Load) 和存储器回存(Store)的CDC6600,电话交换机研究案直截了当指明RISC 指令集第一个要素:存储器载入(Load)/回存(Store)架构。白话点就是「一次从存储器抓一堆运算元(数据)进来,算完再一次丢回存储器」。IBM 在1975 年取消这计算机结构的概念取得相当进展的实验性专案,但当年10 月IBM 决定当成不同应用可共享的通用设计,继续发展,并以研发部门所在地Thomas J.Watson 研究中心建筑物编号为名,命名为IBM 801 计划。日后IBM 801 普遍用于各种IBM 产品,包括System/370 大型主机I/O 通道控制器、各种IBM 网通设备、IBM 9370 大型主机的垂直微码(Vertical Microcode)执行单元,并在日后成为IBM ROMP(Resarch OPD Micro Processor)微处理器、IBM RT PC 工作站和几个内部研究案的技术基础,一步步迈向时下高端RISC 之王:Power(Performance Optimization With Enhanced RISC)。接着更伟大的成就,就堂堂登场了──John Cocke 团队发现软件编译程序都未用到大多数透过微码(Microcode)实作的功能强大复杂的指令。当时可能一个看起来很简单的加法指令,就有一堆对应不同运算元(像寄存器与存储器配对,或不同运算元数量)版本,就是CISC 的特色,为了提供充裕指令数量与功能(受大型主机影响,是那时很重要的商业行销诉求),反而拖慢常用的简单指令。抛弃微码,使用硬件线路(Hardwired)制作这些指令,并有效流水线化(Pipeline),就是RISC 的第二个特征。

 

 ▲ 微码是区分RISC与CISC 的最根本差异,没有之一。即使后来的RISC…...John Cocke 团队总结,这句话也从此颠覆指令集架构潮流:“在计算机与使用者间强加微码(用微码实作「为软件与硬件界面的指令集架构」),会在执行最频繁执行的指令时,产生昂贵的额外负担。”(Imposing microcode between a computer and its users imposes an expensive overhead in performing the most frequently executed instructions.)RISC的重大精神「让最常用的指令跑的更快」(Make The Common Case Fast)由此而生。

 

 ▲ 虽然IBM 并未发明RISC 这名词,但IBM 的801 计划的确早于当代两位大师David Patterson(RISC 名词创造者,之后才出现「对照」用CISC)的RISC I 和John Hennessy 的MIPS。1980 年夏天问世的最初版,时钟频率15.15MHz,理论运算效能高达15MIPS,超出1974 年电话交换机的预定性能需求。但IBM 801 打从娘胎起就是为了功能极有限的系统而生,所以规格也极简单,仅有16 个24 位元数据寄存器,指令编码长度是短短24 位元,没有虚拟存储器,延续古老的2 运算元格式(A = A + B),但2运算元格式(A = A + B) 却会覆盖其中一个,代表必须事先复制一个运算元的数据到另一个寄存器,降低效率。后来IBM 改良多次,特别重要的是将指令编码长度扩展到32 位元,不仅让数据寄存器数量倍增到32 个(用到5 个位元标定),更有足够位元数,改为3 运算元格式(A = B + C),更利于数学应用,因两个数字(B 和C) 都可保留于寄存器,以便重新使用。RISC 第三个象征:「应该」要有32 个数据寄存器和三运算元格式,就这样出现在各位眼前(至于嵌入式导向RISC 指令集,如ARM Thumb,那又是另一个故事了)。更值得注意的是,不限硬件,IBM 801 计划也对改善编译器(Compiler)效率有巨大贡献(各位还记得上一篇ACS 和Frances Allen吗?)。新版IBM 801 于System/370 大型主机以「模拟器」执行时,运行速度竟然会比System/370 原生程式码还快。研发团队将IBM 801「尽其所能利用数据寄存器,并设法减低存取存储器频率」的编译器「逆向」移植回CISC指令集架构的System/370,效能也比原版快三倍,充分证明针对RISC 发展的编译器最佳化技术,也同样可改进CISC 处理器软件执行效能,反正能减少上下其手存储器,都不啻是好事一桩。

总之,从CDC6600、ACS 和IBM 801,能对RISC 做以下总结:1.核心精神:让最常用的指令跑更快。2.RISC「精简」的是指令格式与运算元定址模式,不是指令数目。3.存储器载入(Load)回存(Store)都由专属指令负责。4.尽量使用硬件线路(Hardwired)实做指令,避免采用微码(Microcode)产生控制讯号。5.预设最少要有32个数据寄存器与3 运算元(A = B + C)。6.因应高级语言普及,编译器(Compiler)技术在效能层面扮演举足轻重角色。但IBM 801的传奇并未划下句点,更大挑战即将现身John Cocke 等人眼前:1982 年以IBM 801 的成就为地基,IBM 启动Cheetah(猎豹)计划,借实作多组执行单元,让RISC 在单一时脉周期内执行多道指令,也就是同时执行一个以上指令的「超标量」(Superscalar)流水线,结合让RISC抢滩个人电脑市场的努力(ROMP、RT PC、PowerPC),铺陈蓝色巨人横越超过半世纪的壮丽RISC 发展史。

 

 ▲ 讨厌RISC-V 阵营老爱把RISC 说得好像横空出世的伟大发明,就算不提IBM,RISC 起源甚至可追溯至1964年的CDC6600,距今超过半个世纪。如果还没忘记上一篇提到的IBM System/360,就会马上理解服务器的世界,无论CISC 还是RISC,最顶端的高端产品,依旧清一色是IBM 天下。

 

 ▲ 无预警停机时间排名,足以代替千言万语。不过论世界第一个「兼具超标量、乱序与预测指令执行的RISC 处理器」,一般认定是1990 年IBM Power1(限浮点运算) 或1993 年IBM PowerPC 601,但其实早在1978 年苏联Elbrus-1 实现,只是苏联解体、冷战结束后才逐渐被世人知悉。笔者更好奇的是,当代两位大师合着的两本经典教科书《计算机体系架构:量化研究方法》,何时会探讨俄国人的计算机领域成就?

编译乱序(Compiler Reordering)

编译器(compiler)的工作就是优化代码以提高性能。这包括在不改变程序行为的情况下重新排列指令。因为compiler不知道什么样的代码需要线程安全(thread-safe),所以compiler假设代码都是单线程执行(single-threaded),并且进行指令重排优化并保证是单线程安全的。因此,当你不需要compiler重新排序指令的时候,你需要显式告诉compiler,我不需要重排。否则,它可不会听你的。本篇文章中,一起探究compiler关于指令重排的优化规则。

注:测试使用aarch64-linux-gnu-gcc版本:7.3.0

编译器指令重排(Compiler Instruction Reordering)

compiler的主要工作就是将对人们可读的源码转化成机器语言,机器语言就是对CPU可读的代码。因此,compiler可以在背后做些不为人知的事情。考虑下面的C语言代码:

  1. int a, b;
  2.  
  3. void foo(void)
  4. {
  5.              a = b + 1;
  6.              b = 0;
  7. }

使用aarch64-linux-gnu-gcc在不优化代码的情况下编译上述代码,使用objdump工具查看foo()反汇编结果:

  1. <foo>:
  2.              ...
  3.              ldr         w0, [x0]  // load b to w0
  4.              add        w1, w0, #0x1
  5.              ...
  6.              str         w1, [x0]  // a = b + 1
  7.              ...
  8.              str         wzr, [x0] // b = 0

应该知道Linux默认编译优化选项是-O2,因此采用-O2优化选项编译上述代码,并反汇编得到如下汇编结果:

  1. <foo>:
  2.              ...
  3.              ldr         w2, [x0]  // load b to w2
  4.              str         wzr, [x0] // b = 0
  5.              add        w0, w2, #0x1
  6.              str         w0, [x1]  // a = b + 1
  7.              ...

比较优化和不优化的结果,可以发现。在不优化的情况下,a 和 b 的写入内存顺序符合代码顺序(program order)。但是-O2优化后,a 和 b 的写入顺序和program order是相反的。-O2优化后的代码转换成C语言可以看作如下形式:

  1. int a, b;
  2.  
  3. void foo(void)
  4. {
  5.              register int reg = b;
  6.  
  7.              b = 0;
  8.              a = reg + 1;
  9. }

这就是compiler reordering(编译器重排)。为什么可以这么做呢?对于单线程来说,a 和 b 的写入顺序,compiler认为没有任何问题。并且最终的结果也是正确的(a == 1 && b == 0)。

这种compiler reordering在大部分情况下是没有问题的。但是在某些情况下可能会引入问题。例如使用一个全局变量flag标记共享数据data是否就绪。由于compiler reordering,可能会引入问题。考虑下面的代码(无锁编程):

  1. int flag, data;
  2.  
  3. void write_data(int value)
  4. {
  5.     data = value;
  6.     flag = 1;
  7. }

如果compiler产生的汇编代码是flag比data先写入内存。那么,即使是单核系统上,也会有问题。在flag置1之后,data写45之前,系统发生抢占。另一个进程发现flag已经置1,认为data的数据已经准别就绪。但是实际上读取data的值并不是45。为什么compiler还会这么操作呢?因为,compiler是不知道data和flag之间有严格的依赖关系。这种逻辑关系是人为强加的。如何避免这种优化呢?

显式编译器屏障(Explicit Compiler Barriers)

为了解决上述变量之间存在依赖关系导致compiler错误优化。compiler为提供了编译器屏障(compiler barriers),可用来告诉compiler不要reorder。继续使用上面的foo()函数作为演示实验,在代码之间插入compiler barriers。

  1. #define barrier() __asm__ __volatile__("": : :"memory")
  2.  
  3. int a, b;
  4.  
  5. void foo(void)
  6. {
  7.              a = b + 1;
  8.              barrier();
  9.              b = 0;
  10. }
 

barrier()就是compiler提供的屏障,作用是告诉compiler内存中的值已经改变,之前对内存的缓存(缓存到寄存器)都需要抛弃,barrier()之后的内存操作需要重新从内存load,而不能使用之前寄存器缓存的值。并且可以防止compiler优化barrier()前后的内存访问顺序。barrier()就像是代码中的一道不可逾越的屏障,barrier前的 load/store 操作不能跑到barrier后面;同样,barrier后面的 load/store 操作不能在barrier之前。依然使用-O2优化选项编译上述代码,反汇编得到如下结果:

  1. <foo>:
  2.              ...
  3.              ldr         w2, [x0]  // load b to w2
  4.              add        w2, w2, #0x1
  5.              str         w2, [x1]  // a = a + 1
  6.              str         wzr, [x0] // b = 0
  7.              ...

可以看到插入compiler barriers之后,a 和 b 的写入顺序和program order一致。因此,当代码中需要严格的内存顺序,就需要考虑compiler barriers。

隐式编译器屏障(Implied Compiler Barriers)

除了显示的插入compiler barriers之外,还有别的方法阻止compiler reordering。例如CPU barriers 指令,同样会阻止compiler reordering。后续再考虑CPU barriers。

除此以外,当某个函数内部包含compiler barriers时,该函数也会充当compiler barriers的作用。即使这个函数被inline,也是这样。例如上面插入barrier()的foo()函数,当其他函数调用foo()时,foo()就相当于compiler barriers。考虑下面的代码:

  1. int a, b, c;
  2.  
  3. void fun(void)
  4. {
  5.              c = 2;
  6.              barrier();
  7. }
  8.  
  9. void foo(void)
  10. {
  11.              a = b + 1;
  12.              fun();                   /* fun() call act as compiler barriers */
  13.              b = 0;
  14. }

fun()函数包含barrier(),因此foo()函数中fun()调用也表现出compiler barriers的作用。同样可以保证 a 和 b 的写入顺序。如果fun()函数不包含barrier(),结果又会怎么样呢?实际上,大多数的函数调用都表现出compiler barriers的作用。但是,这不包含inline的函数。因此,fun()如果被inline进foo(),那么fun()就不会具有compiler barriers的作用。如果被调用的函数是一个外部函数,其副作用会比compiler barriers还要强。因为compiler不知道函数的副作用是什么。它必须忘记它对内存所作的任何假设,即使这些假设对该函数可能是可见的。我么看一下下面的代码片段,printf()一定是一个外部的函数。

  1. int a, b;
  2.  
  3. void foo(void)
  4. {
  5.              a = 5;
  6.              printf("smcdef");
  7.              b = a;
  8. }

同样使用-O2优化选项编译代码,objdump反汇编得到如下结果。

  1. <foo>:
  2.              ...
  3.              mov       w2, #0x5                           // #5
  4.              str         w2, [x19]                           // a = 5
  5.              bl          640 <__printf_chk@plt>                   // printf()
  6.              ldr         w1, [x19]                           // reload a to w1
  7.              ...
  8.              str         w1, [x0]                             // b = a

compiler不能假设printf()不会使用或者修改 a 变量。因此在调用printf()之前会将 a 写5,以保证printf()可能会用到新值。在printf()调用之后,重新从内存中load a 的值,然后赋值给变量 b。重新load a 的原因是compiler也不知道printf()会不会修改 a 的值。

因此,可以看到即使存在compiler reordering,但是还是有很多限制。当需要考虑compiler barriers时,一定要显示的插入barrier(),而不是依靠函数调用附加的隐式compiler barriers。因为,谁也无法保证调用的函数不会被compiler优化成inline方式。

barrier()除了防止编译乱序,还没能做什么

barriers()作用除了防止compiler reordering之外,还有什么妙用吗?考虑下面的代码片段。

  1. int run = 1;
  2.  
  3. void foo(void)
  4. {
  5.              while (run)
  6.                           ;
  7. }

run是个全局变量,foo()在一个进程中执行,一直循环。期望的结果时foo()一直等到其他进程修改run的值为0才推出循环。实际compiler编译的代码和会达到预期的结果吗?看一下汇编代码。

  1. 0000000000000748 <foo>:
  2.  748:     90000080            adrp      x0, 10000
  3.  74c:      f947e800             ldr         x0, [x0, #4048]
  4.  750:     b9400000            ldr         w0, [x0]                                               // load run to w0
  5.  754:     d503201f            nop
  6.  758:     35000000            cbnz      w0, 758 <foo+0x10>          // if (w0) while (1);
  7.  75c:      d65f03c0             ret

汇编代码可以转换成如下的C语言形式。

  1. int run = 1;
  2.  
  3. void foo(void)
  4. {
  5.              register int reg = run;
  6.  
  7.              if (reg)
  8.                           while (1)
  9.                                        ;
  10. }

compiler首先将run加载到一个寄存器reg中,然后判断reg是否满足循环条件,如果满足就一直循环。但是循环过程中,寄存器reg的值并没有变化。因此,即使其他进程修改run的值为0,也不能使foo()退出循环。很明显,这不是想要的结果。继续看一下加入barrier()后的结果。

  1. 0000000000000748 <foo>:
  2.  748:     90000080            adrp      x0, 10000
  3.  74c:      f947e800             ldr         x0, [x0, #4048]
  4.  750:     b9400001            ldr         w1, [x0]                                               // load run to w0
  5.  754:     34000061            cbz        w1, 760 <foo+0x18>
  6.  758:     b9400001            ldr         w1, [x0]                                               // load run to w0
  7.  75c:      35ffffe1 cbnz      w1, 758 <foo+0x10>          // if (w0) goto 758
  8.  760:     d65f03c0             ret

可以看到加入barrier()后的结果真是想要的。每一次循环都会从内存中重新load run的值。因此,当有其他进程修改run的值为0的时候,foo()可以正常退出循环。为什么加入barrier()后的汇编代码就是正确的呢?因为barrier()作用是告诉compiler内存中的值已经变化,后面的操作都需要重新从内存load,而不能使用寄存器缓存的值。因此,这里的run变量会从内存重新load,然后判断循环条件。这样,其他进程修改run变量,foo()就可以看得见了。

在Linux kernel中,提供了cpu_relax()函数,该函数在ARM64平台定义如下:

  1. static inline void cpu_relax(void)
  2. {
  3.              asm volatile("yield" ::: "memory");
  4. }

可以看出,cpu_relax()是在barrier()的基础上又插入一条汇编指令yield。在kernel中,经常会看到一些类似上面举例的while循环,循环条件是个全局变量。为了避免上述所说问题,就会在循环中插入cpu_relax()调用。

  1. int run = 1;
  2.  
  3. void foo(void)
  4. {
  5.              while (run)
  6.                           cpu_relax();
  7. }

当然也可以使用Linux 提供的READ_ONCE()。例如,下面的修改也同样可以达到预期的效果。

  1. int run = 1;
  2.  
  3. void foo(void)
  4. {
  5.              while (READ_ONCE(run))    /* similar to while (*(volatile int *)&run) */
  6.                           ;
  7. }

当然你也可以修改run的定义为volatile int run就会得到如下代码。同样可以达到预期目的。

  1. volatile int run = 1;
  2.  
  3. void foo(void)
  4. {
  5.              while (run)
  6.                           ;
  7. }

关于volatile更多使用建议可以参考这里。

解读vue-template-compiler

  • 导读

在写Vue 单文件组件的时候,需要知道是是使用vue-loader和vue-template-compiler 对vue文件进行的处理。Vue-loader是webpack的插件,因为Vue2 版本是基于webpack打包的。准备的说,vue-cli底层使用的是webpack。Vue-loader更多的需要去处理静态资源相关的信息,而vue-template-compiler 是则是专门处理vue template 文件的工具,后面的内容则是主要分析vue-template-compiler 中的compiler 过程和实现一个最小化的vue-template-compiler;

  • vue-template-compiler 工程分析 当分析一个源码库的时候,面对如此多的代码,首先不能没有头绪,在笔者看来,比较合适的方式是找出口,对于node 环境的库,最后执行导出:exports.xxx = xxx; 这个就是对外暴露的用法,所以第一步找到头,这个头找到了,接下来需要顺藤摸瓜,找到与之相关的完整逻辑,按照这个方法,分析vue-template-compiler:
    function createCompilerCreator (baseCompile) {
      return function createCompiler (baseOptions) {
        function compile (
          template,
          options
        ) {
          var finalOptions = Object.create(baseOptions);
          var errors = [];
          var tips = [];

          var warn = function (msg, range, tip) {
            (tip ? tips : errors).push(msg);
          };

          if (options) {
            if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
              // $flow-disable-line
              var leadingSpaceLength = template.match(/^\s*/)[0].length;

              warn = function (msg, range, tip) {
                var data = { msg: msg };
                if (range) {
                  if (range.start != null) {
                    data.start = range.start + leadingSpaceLength;
                  }
                  if (range.end != null) {
                    data.end = range.end + leadingSpaceLength;
                  }
                }
                (tip ? tips : errors).push(data);
              };
            }
            // merge custom modules
            if (options.modules) {
              finalOptions.modules =
                (baseOptions.modules || []).concat(options.modules);
            }
            // merge custom directives
            if (options.directives) {
              finalOptions.directives = extend(
                Object.create(baseOptions.directives || null),
                options.directives
              );
            }
            // copy other options
            for (var key in options) {
              if (key !== 'modules' && key !== 'directives') {
                finalOptions[key] = options[key];
              }
            }
          }

          finalOptions.warn = warn;

          var compiled = baseCompile(template.trim(), finalOptions);
          if (process.env.NODE_ENV !== 'production') {
            detectErrors(compiled.ast, warn);
          }
          compiled.errors = errors;
          compiled.tips = tips;
          return compiled
        }

        return {
          compile: compile,
          compileToFunctions: createCompileToFunctionFn(compile)
        }
      }
    }


    var createCompiler = createCompilerCreator(function baseCompile (
      template,
      options
    ) {
      var ast = parse(template.trim(), options);
      if (options.optimize !== false) {
        optimize(ast, options);
      }
      var code = generate(ast, options);
      return {
        ast: ast,
        render: code.render,
        staticRenderFns: code.staticRenderFns
      }
    });

    var ref = createCompiler(baseOptions);
    var compile = ref.compile;

    exports.compile = compile;

compiler 找到了,接下来寻找谁创建的compiler , 是createCompiler函数创建的compiler,createCompilerCreator 函数又创建了createCompiler , 所以最终找到了来龙去脉。当前,函数的内部依赖了很多的函数,这里并没有声明定义,所以执行的时候肯定会报错。接下来的工作就需要一步一步将一些判断、边界函数的定义声明到文件中。

这里就不展开这个工作了,至于代码,笔者会在最后补充上。接下来,就需要分析vue-template-compiler 做了哪些事情。

 

 

 执行了compiler 的基本过程,那么,在继续聚焦,聚焦parse 和 generate 的实现。

parse:

/**
* Convert HTML string to AST.
*/
function parse (
 template,
 options
) {
 warn$1 = options.warn || baseWarn;

 platformIsPreTag = options.isPreTag || no;
 platformMustUseProp = options.mustUseProp || no;
 platformGetTagNamespace = options.getTagNamespace || no;
 var isReservedTag = options.isReservedTag || no;
 maybeComponent = function (el) { return !!(
   el.component ||
   el.attrsMap[':is'] ||
   el.attrsMap['v-bind:is'] ||
   !(el.attrsMap.is ? isReservedTag(el.attrsMap.is) : isReservedTag(el.tag))
 ); };
 transforms = pluckModuleFunction(options.modules, 'transformNode');
 preTransforms = pluckModuleFunction(options.modules, 'preTransformNode');
 postTransforms = pluckModuleFunction(options.modules, 'postTransformNode');

 delimiters = options.delimiters;

 var stack = [];
 var preserveWhitespace = options.preserveWhitespace !== false;
 var whitespaceOption = options.whitespace;
 var root;
 var currentParent;
 var inVPre = false;
 var inPre = false;
 var warned = false;

 function warnOnce (msg, range) {
     ...
 }

 function closeElement (element) {
   trimEndingWhitespace(element);
   if (!inVPre && !element.processed) {
     element = processElement(element, options);
   }
   // tree management
   if (!stack.length && element !== root) {
     // allow root elements with v-if, v-else-if and v-else
     if (root.if && (element.elseif || element.else)) {
       if (process.env.NODE_ENV !== 'production') {
         checkRootConstraints(element);
       }
       addIfCondition(root, {
         exp: element.elseif,
         block: element
       });
     } else if (process.env.NODE_ENV !== 'production') {
       warnOnce(
         "Component template should contain exactly one root element. " +
         "If you are using v-if on multiple elements, " +
         "use v-else-if to chain them instead.",
         { start: element.start }
       );
     }
   }
   if (currentParent && !element.forbidden) {
     if (element.elseif || element.else) {
       processIfConditions(element, currentParent);
     } else {
       if (element.slotScope) {
         // scoped slot
         // keep it in the children list so that v-else(-if) conditions can
         // find it as the prev node.
         var name = element.slotTarget || '"default"'
         ;(currentParent.scopedSlots || (currentParent.scopedSlots = {}))[name] = element;
       }
       currentParent.children.push(element);
       element.parent = currentParent;
     }
   }

   // final children cleanup
   // filter out scoped slots
   element.children = element.children.filter(function (c) { return !(c).slotScope; });
   // remove trailing whitespace node again
   trimEndingWhitespace(element);

   // check pre state
   if (element.pre) {
     inVPre = false;
   }
   if (platformIsPreTag(element.tag)) {
     inPre = false;
   }
   // apply post-transforms
   for (var i = 0; i < postTransforms.length; i++) {
     postTransforms[i](element, options);
   }
 }

 function trimEndingWhitespace (el) {
   // remove trailing whitespace node
   ...
 }

 function checkRootConstraints (el) {
   if (el.tag === 'slot' || el.tag === 'template') {
     warnOnce(
       "Cannot use <" + (el.tag) + "> as component root element because it may " +
       'contain multiple nodes.',
       { start: el.start }
     );
   }
   if (el.attrsMap.hasOwnProperty('v-for')) {
     warnOnce(
       'Cannot use v-for on stateful component root element because ' +
       'it renders multiple elements.',
       el.rawAttrsMap['v-for']
     );
   }
 }

 });
 return root
}

parse 函数的代码有点多,主要的逻辑在于函数parseHTML函数,该函数主要是将HTML代码转换,转换的时候需要注意开关的tag等,也就是说所有的处理都是在该函数上。

parse过程结束之后,接下来是generate过程。该过程主要是将代码规格化【格式化】,便于输出最后的{ast, render, staticRenderFns };

整个过程下来,就是vue-template-compiler 中的compiler 的主要逻辑了。

  • mini-vue-template-compiler compiler 实现

compiler 实现的代码见github;

  • 结语

因为平时业务用vue 开发比较的多 ,所以会分析一下vue-template-compiler 的实现过程,后续会继续分析vue-loader, 这是孪生兄弟,少不了的。

 

 

参考文献链接

https://mp.weixin.qq.com/s/0O7BFr6Jh1JTuK7d-icRsA

https://mp.weixin.qq.com/s/ZLS317bjT3teuzE_ELpgGA

https://mp.weixin.qq.com/s/mh2P34tdkRaYMZ9I9yoztA

posted @ 2022-11-09 04:53  吴建明wujianming  阅读(196)  评论(0编辑  收藏  举报