最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • Vue源码解析-compiler

    正文概述 掘金(老刘大话前端)   2021-03-14   453

    上一篇,我们介绍了vue实现响应式的原理。其中,有2点需要重点展开分析:

      1. vue组件化实现
      1. render函数执行过程中发生了什么

    在讨论上述2个问题之前,我们先待 了解 compiler过程。这是核心前提,只有先熟悉了它,我们才能清晰的认识到 数据的流向。

    好了,废话不多说,扭起袖子就是干~

    一. Compiler

    在vue中,我们写template,显然浏览器不认识。那么就需要有个解释过程。

    compiler 分两种情况:

      1. 构建时compiler
      1. 运行时compiler

    构建时compiler

    本地开发时,使用webpack + vue-loader,来处理.vue文件。 例如:

    <template>
      <div>{{ a }}</div>
    </template>
    
    <script>
    export default {
      data() {
        return {
          a: '1'
        }
      }
    }
    </script>
    
    <style>
    
    </style>
    

    打包时,vue-loader会 将 .vue文件的内容,转化为render函数

    运行时compiler

    运行时compiler,我们 不使用 vue-loader这样的插件,而且直接写template,让vue在浏览器运行的时候,动态将template转化为render函数。例如:

    <html>
      <head>
        <meta charset="utf-8"/>
      </head>
    
      <body>
        <div id='root'>
    
        </div>
        <script src="../vue/dist/vue.js"></script>
        <script>
    
          let vm = new Vue({
            el: '#root',
            template: '<div>{{ a }}</div>',
            data() {
              return {
                a: "这是根节点"
              }
            }
          })
          
        </script>
      </body>
    </html>
    

    本质上,构建时compiler和运行时compiler都是转化为render函数。 显示构建时效率更高,在我们的生产环境中,尽量避免运行的时候,再去compiler。

    细心的同学会问了:既然都是转化为render函数,那是不是也可以手写render函数?

    答案是肯定的,例如:

    <html>
      <head>
        <meta charset="utf-8"/>
      </head>
    
      <body>
        <div id='root'>
    
        </div>
        <script src="../vue/dist/vue.js"></script>
        <script>
    
          let vm = new Vue({
            el: '#root',
            data() {
              return {
                a: "这是根节点"
              }
            },
            render(createElement) {
              return createElement('div', {
                attrs: {
                  id: 'test'
                }
              }, this.a)
            }
          })
        </script>
      </body>
    </html>
    

    手写render,vue会直接执行render, 省去了compiler过程。 但是手写render,对于我们开发和维护 都不友好。还是建议大家 使用 webpack + vue-loader,构建时compiler。

    另外,如果是学习的话,建议运行时compiler。

    下面,我们将采用运行时compiler,来一探究竟。

    Vue源码解析-compiler

    二. compileToFunctions

    mount挂载时,如果没有传入render函数,vue会 先执行 compileToFunctions函数,返回render函数,并将render函数,挂载到vm.$options上。以便后续 执行 patch之前,生成虚拟dom。

    核心主流程代码如下:

    export const createCompiler = createCompilerCreator(function baseCompile (
      template: string,
      options: CompilerOptions
    ): CompiledResult {
      const ast = parse(template.trim(), options)
      if (options.optimize !== false) {
        optimize(ast, options)
      }
      const code = generate(ast, options)
      return {
        ast,
        render: code.render,
        staticRenderFns: code.staticRenderFns
      }
    })
    

    总体流程分为三部分:

      1. 模板编译阶段
      1. 优化阶段
      1. 代码生成阶段 - 即转化为render函数

    下面,我们将逐个击破

    三. AST

    ast 全名:Abstract Syntax Tree,即抽象语法树。是源代码语法结构的一种抽象表示。

    在计算机中,任何问题的本质就是 数据结构 + 算法,ast也是一种数据结构,来描述源代码的一种结构化表示。

    以我们上面的运行时demo为例:

    parse的方法入参,第一个参数是template,是一个字符串, 即:

    "<div data-test='这是测试属性'>{{ a }}</div>"
    

    抽丝剥茧,我们先看到ast生成阶段parse方法 的核心入口:

    // ... 省略一堆函数定义
    parseHTML(template, {
      // options
      ...,
      
      start() {
         // ...
      },
      
      end() {
        // ...
      },
      
      chars() {
        // ...
      },
      comment() {
        // ...
      }
    })
    

    parseHTML 主干如下:

    export function parseHTML (html, options) {
      const stack = []
      // ...options
      let index = 0
      let last, lastTag;
      
      while(html) {
        last = html
        
        if (!lastTag || !isPlainTextElement(lastTag)) {
          let textEnd = html.indexOf('<')
          
          if(textEnd == 0) {
            
            if(comment.test(html)) {
              const commentEnd = html.indexOf('-->')
              // ...
              if(commentEnd >= 0) {
                // ...
                advance(commentEnd + 3)
                continue
              }
            }
            
            if(conditionalComment.test(html)) {
              // ...
              const conditionalEnd = html.indexOf(']>')
              if (conditionalEnd >= 0) {
                advance(conditionalEnd + 2)
                continue
              }
            }
            
            if(html.match(doctype)) {
              // ...
              advance(doctypeMatch[0].length)
              continue
            }
            
            if(html.match(endTag)) {
              // ...
              const curIndex = index
              advance(endTagMatch[0].length)
              parseEndTag(endTagMatch[1], curIndex, index)
              continue
            }
            
            startTagMatch = parseStartTag()
            if(startTagMatch) {
              // ...
              handleStartTag(startTagMatch)
              if (shouldIgnoreFirstNewline(startTagMatch.tagName, html)) {
                advance(1)
              }
              continue
            }
          }
          
          if(textEnd >= 0) {
            // 如果 < 出现在 纯文本中,需要宽容处理,先做为文本处理
          }
          
          if(textEnd < 0) {
            // ...赋值text, advance跳过文本长度
          }
        }
      }else {
        // 处理script,style,textarea的元素
        // 这里我们只处理textarea元素, 其他的两种Vue 会警告,不提倡这么写
        // ...
      }
      
      function advance (n) {
        index += n
        html = html.substring(n)
      }
    }
    

    我们可以看到parseHTML里面,实际上写了许多正则,去处理字符串。 这个其实不是尤大 从零写起, 尤大是参考 大神 John Resig 之前写的html parse库。

    John Resig 何许人也? 正是大名鼎鼎的 JQuery之父。经历过 jquery时代的人, 那个时候jquery是神一般 的存在。

    好了,回归正题,template字符串处理,大致流程如下:

      1. while循环 template 字符串
      1. 判断不能是 script, style这些标签,给出对应的警告信息
      1. 通过正则,获取开始标签 < 的字符串位置
      1. 通过正则,判断是否是注释节点,调用advance方法,重新记录index下标,跳过注释长度,截取去注释继续循环
      1. 通过正则,判断是否是条件注释节点。因为我们可能在template中使用条件注释,针对ie做一些事件。同理,调用advance方法,将index下标,跳转到条件注释字符串的尾部,截取掉条件注释,继续循环。
      1. 通过正则,判断是否是 Doctype 节点,同理,调用advance方法,将index下标跳转到 doctype 节点字符串尾部,截取掉 doctype, 继续循环。
      1. 通过正则,判断是否是开始标签,将开始标签的内容提取出来,提取前后对比:
    // 匹配开始标签之前
    html = "<div data-test='这是测试属性'>{{ a }}</div>"
    
    // 提取之后
    html =  "{{ a }}</div>"
    
    // 而此时 startTagMatch 变成这样:
    {
      start: 0,
      end: 24,
      tagName: 'div',
      unarySlash: '',
      attrs: [
        "data-test='这是测试属性'",
        // ...
      ]
    }
    

    开始标签解析完成 后,会调用 parseHTML第二个参数options上的start方法,即上面提到的 parseHTML调用代码:

    // ... 省略一堆函数定义
    parseHTML(template, {
      // options
      ...,
      
      start(tag, attrs, unary, start, end) {
         // 调用这里的start方法,可以理解成,每次parse一部分html字符串,都会调用本次的 生命周期函数,start, end, chars, comment
         
         // ...
         let element: ASTElement = createASTElement(tag, attrs, currentParent)
         
         // ...
      },
      
      end() {
         
      },
      
      // ...
     
    })
    

    根据上面的流程,我们已经知道,parseHtml会先 提取出 开始标签相关内容,即:

    <div data-test='这是测试属性'>
    

    然后根据startTagMatch数据,调用start方法,start方法调用createASTElement 返回astElement。其结构如下:

    {
      type: 1,
      tag: 'dev',
      rawAttrsMap: {},
      parent: undefined,
      children: [],
      attrsMap: {
        "data-test": "这是测试属性"
      },
      attrsList: [
        {
          start: 5,
          end: 23,
          name: 'data-test',
          value: "这是测试属性"
        }
      ]
    }
    
      1. 开始标签内容处理完成后,去除开始内容后的字符串,变成这样:
    html = "{{ a }}</div>"
    

    进入下一个while循环,剩下的字符串,继续做为html字段值,再去走一遍以上流程。 此时会进入 :

    if(textEnd >= 0) {
    
    }
    

    text变量会记录下来,即:

    text = "{{ a }}"
    

    调用advance,将index调至text字符串的尾部,截取掉{{ a }}

      1. 再次进入下一个while循环,即:
    html = "</div>"
    

    重复上面的过程,条件匹配到了结束标签,进入:endTagMatch,即"</div>"。 调用advance方法,将index移动到最后。 调用 parseEndTag 方法,触发end钩子。

    advance相当于一个下标计算器,每解析完一步,就自动的移动到 之前解析过的尾部,开始下一部分解析

    简单的理解就是:parseHTML方法,一边解析不同的内容一边调用对应的钩子函数生成对应的AST节点,最终完成将整个模板字符串转化成AST

    总体来说,ast的类型,有3类。

    1. 正常标签节点处理,通过createASTElement 方法创建,其结构如下:
    {
      type: 1,
      tag,
      attrsList: [
        // ...
      ],
      attrsMap: {
        // ...
      },
      rawAttrsMap: {},
      parent,
      children: []
    }
    
    1. 匹配到字符变量相关,使用parseText解释器,其结构如下:
    {
      type: 2,
      text: "{{ a }}",
      expression: "_s(a)",
      tokens: [
        {
          '@binding': 'a'
        }
      ],
      start: 24,
      end: 31
    }
    
    1. 纯文本,不包含变量,其结构如下:
    {
      type: 3,
      text: "文本内容"
      isComment: true,
      start: xx,
      end: xx
    }
    

    最终,template字符串,解析出来的ast结构如下:

    {
        "type": 1,
        "tag": "div",
        "attrsList": [
            {
               "name": "data-test",
               "value": "这是测试属性",
               "start": 5,
               "end": 23
    	}
          ],
          "attrsMap": {
    	"data-test": "这是测试属性"
          },
           "rawAttrsMap": {
    	  "data-test": {
    	    "name": "data-test",
    	    "value": "这是测试属性",
    	    "start": 5,
    	    "end": 23
    	  }
    	},
    	"children": [
              {
    	    "type": 2,
    	    "expression": "_s(a)",
    	    "tokens": [
                  {
    		"@binding": "a"
    	      }
                ],
    	    "text": "{{ a }}",
    	    "start": 24,
                "end": 31
    	  }
            ],
    	"start": 0,
    	"end": 37,
    	"plain": false,
    	"attrs": [
              {
                "name": "data-test",
    	    "value": "\"这是测试属性\"",
    	    "start": 5,
    	    "end": 23
    	  }
            ]
    }
    

    需要说明是,我们看到expression 中有个_s, 这个是什么东西呢? 实际上,这个是在vue instance的 render-helpers中定义的。_s = toString

    其定义如下:

    export function toString (val: any): string {
      return val == null
        ? ''
        : Array.isArray(val) || (isPlainObject(val) && val.toString === _toString)
          ? JSON.stringify(val, null, 2)
          : String(val)
    }
    

    ok, 到这里, ast的主干流程就结束了。

    四. optimize

    获取到ast树后,vue做了一层静态标记优化。给一些不变的节点打上标记,提升后面patch diff的性能。比如,有这样的标签:

    <div>
      <div>这是不变的内容1</div>
      <div>这是不变的内容2</div>
      <div>这是不变的内容3</div>
      <div>这是不变的内容4</div>
    </div>
    

    那么,在进行diff的时候,这种标签都不需要比对,他是纯静态的标签,不会变化。最外层的div,称为:静态根节点。

    根据上面生成的ast,我们知道有3种类型的 ast,分别是:

    1. type == 1, 普通元素节点
    2. type == 2, 包含变量的文本
    3. type == 3, 纯文本,不包含变量

    由此可见,将type = 3的 ast都加上static = true标识 type = 2的ast,都加上static = false标识 type = 1的,需要进一步判断:

    function isStatic (node: ASTNode): boolean {
      if (node.type === 2) { // expression
        return false
      }
      if (node.type === 3) { // text
        return true
      }
      return !!(node.pre || (
        !node.hasBindings && // no dynamic bindings
        !node.if && !node.for && // not v-if or v-for or v-else
        !isBuiltInTag(node.tag) && // not a built-in
        isPlatformReservedTag(node.tag) && // not a component
        !isDirectChildOfTemplateFor(node) &&
        Object.keys(node).every(isStaticKey)
      ))
    }
    

    即:

      1. 如果节点使用了v-pre指令,那就断定它是静态节点;
      1. 没有pre,需要满足以下条件:
      • 2.1 不能有v-,@, :开头属性
      • 2.2 不能是内置的slot, component
      • 2.3 必须是浏览器保留标签,不能是组件
      • 2.4 不能是v-for的template标签
      • 2.5 判断ast上每个key,是不是只有静态节点才有

    标计完成之后,再去ast上,递归每个children进行标记。

    在此基础之上,计算出根静态节点,那么diff时候,是根静态节点,那么这个根节点以下的内容都不需要再比较了。

    优化完成后,ast的结构变成这样:(多了2个属性)

    {
        "type": 1,
        "tag": "div",
        // 这里添加静态标记
        "static": false,
        // 这里添加是否是根静态节点标记
        "staticRoot": false,
        "attrsList": [
            {
               "name": "data-test",
               "value": "这是测试属性",
               "start": 5,
               "end": 23
    	}
          ],
          "attrsMap": {
    	"data-test": "这是测试属性"
          },
           "rawAttrsMap": {
    	  "data-test": {
    	    "name": "data-test",
    	    "value": "这是测试属性",
    	    "start": 5,
    	    "end": 23
    	  }
    	},
    	"children": [
              {
                // 这里添加标记
                "static": false,
    	    "type": 2,
    	    "expression": "_s(a)",
    	    "tokens": [
                  {
    		"@binding": "a"
    	      }
                ],
    	    "text": "{{ a }}",
    	    "start": 24,
                "end": 31
    	  }
            ],
    	"start": 0,
    	"end": 37,
    	"plain": false,
    	"attrs": [
              {
                "name": "data-test",
    	    "value": "\"这是测试属性\"",
    	    "start": 5,
    	    "end": 23
    	  }
            ]
    }
    

    五. generate

    代码生成阶段,通过ast将转化为render函数,其代码如下:

    export function generate (
      ast: ASTElement | void,
      options: CompilerOptions
    ): CodegenResult {
      const state = new CodegenState(options)
      const code = ast ? genElement(ast, state) : '_c("div")'
      return {
        render: `with(this){return ${code}}`,
        staticRenderFns: state.staticRenderFns
      }
    }
    

    生成代码阶段比较清晰,genElement方法,主要是判断不同的类型,调用不同的生成方法。本质上是一个json转化成另外一个json。值得注意的是,转化json的过程中,我们会看到 _m, _c, _o的这些方法。

    这些可以在instance/render-helpers/index.js中可查看到

    方法名对应的helper方法
    _mrenderStatic_omarkOnce_lrenderList_ecreateEmptyVNode_trenderSlot_bbindObjectProps_vcreateTextVNode_stoString

    这里生成的render函数如下:

    with(this) {
        return _c('div', {
            attrs: {
                "data-test": "这是测试属性"
            }
        },
        [_v(_s(a))])
    }
    

    到这里, compiler过程就结束啦。这个时候,compiler出的render函数,将挂载到 vm.$options上。等待执行updateComponent方法执行时,生成虚拟dom。

    注意:compiler只是返回render函数,并未执行render函数,所以这个阶段,还未触发Dep类的依赖收集

    六. 总结

      1. compiler是将template字符串转化为render函数的过程
      1. 调用parse方法生成ast
      • 2.1 parseHTML通过正则动态匹配出标签的开始内容,标签内内容,标签结束内容
      • 2.2 不建议template中出现script, style标签,给出警告
      • 2.3 从index = 0开始,匹配开始标签内容,调用advance将index移动至前一次的字符串末尾位置,返回出对应的数据结构描述标签开始内容。另外调用parse的开始生命周期函数,生成对应的 ast
      • 2.4 分别处理 注释节点, 条件注释,Doctype节点,调用advance将index移动到特殊节点字符串的末尾
      • 2.5 while循环计算下一个字符串类型,匹配标签内容
      • 2.6 标签内容调用 parse生命周期的chars方法,生成对应的ast
      • 2.7 匹配结束标签,调用advance将index移动到对应字符串尾部,调用parse的end生命 周期方法,更新对应ast的end标识位
      • 2.8 如此往复调用,直到解析html字符串的最后。
      1. 优化ast,给各个节点的ast打上静态标记,以及静态 根节点,以便patch过程做diff时,去除不必要的对比,提升性能。
      1. 将ast的数据结构,递归遍历每个childrens,将其转化为对应的方法调用。
      1. 返回render函数,将方法挂载至vm.$options上,等待后面执行到updateComponent时生成虚拟DOM

    码字不易,多多关注,点赞 Thanks♪(・ω・)ノ


    起源地下载网 » Vue源码解析-compiler

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元