Vue编译器源码分析

654 阅读2分钟

编译器思维导图

$mount重复定义的原因

  /**
   * 组件进行挂载
   * 
   */
  Vue.prototype.$mount = function (
    el,
    hydrating
  ) {
    el = el && inBrowser ? query(el) : undefined;
    return mountComponent(this, el, hydrating)
  };

   //将mount进行缓存
   var mount = Vue.prototype.$mount;

   //$mount的重新定义
   Vue.prototype.$mount = function (
     el,
     hydrating
   ) {
     el = el && query(el);  
   
        //编译器入口
         var ref = compileToFunctions(template, {
           outputSourceRange: "development" !== 'production',
           shouldDecodeNewlines: shouldDecodeNewlines,
           shouldDecodeNewlinesForHref: shouldDecodeNewlinesForHref,
           delimiters: options.delimiters,
           comments: options.comments
         }, this); 
     return mount.call(this, el, hydrating)
   };
 

从上面的源码可以看到vue对$mount的定义有个重复定义的过程,是解决runtime-only和runtime-compiler的问题,是否需要模板编译;

编译器生成

  //编译器创建过程
  var createCompiler = createCompilerCreator(function baseCompile (
    template,
    options
  ) {
    var ast = parse(template.trim(), options);//生成抽象语法树  AST
    if (options.optimize !== false) {
      optimize(ast, options); //标注静态节点
    }
    var code = generate(ast, options); //生成渲染函数所需要的字符串
    return {
      ast: ast,
      render: code.render,
      staticRenderFns: code.staticRenderFns
    }
  });
  1. 执行parse,生成AST;
  2. 标注静态节点;
  3. 执行generate生成渲染函数所需字符串;

模板到AST

  1. parse完成了模板到AST的生成;
/**
 * 
 * @param {*} template  解析模板
 * @param {*} options   编译器参数
 */
  function parse (
    template,
    options
  ) {     
    parseHTML(template, {    
    });
    return root
  }
  1. parse中主要执行了parseHTML;
 /**
     * 1. 解析模板,提取信息(提取单个),生成初步的AST对象
     * 2. 调用钩子函数  start  end  chars comment  构建模板与AST之间的父子关系
     */
  function parseHTML (html, options) {
    var stack = [];//检测一个非一元标签是否缺少结束标签
    var expectHTML = options.expectHTML;
    var isUnaryTag?1 = options.isUnaryTag || no;//当前解析的标签是否为一元标签
    var canBeLeftOpenTag?1 = options.canBeLeftOpenTag || no;
    var index = 0;
    var last, lastTag;//还未编译的字符串
    //不会进入死循环的原因,正则方式建立token,提取信息,在原始字符串中删除
    while (html) {        
    }  

    //删除html中匹配的token,如开始标签:<div
    function advance (n) {
      index += n;
      html = html.substring(n);
    }

    //attribute 属性的正则文法
    function parseStartTag () {      
    }

    /**
     * 辅助函数  对于特殊标签的处理如p  一个</p>浏览器自动生成<p></p>
     * @param {} match 
     */
    function handleStartTag (match) { 
    }    
  }

静态节点标注

optimize的目标是遍历生成的模板AST树,检测纯静态的子树,即永远不需要更改的DOM。一旦我们检测到这些子树,我们可以:

  1. 把它们变成常数,这样我们就不需要了在每次重新渲染时为它们创建新的节点;
  2. 在修补过程中完全跳过它们;

以上方法判断是否是静态节点:

  1. 如果是纯文本,则标记为静态节点;
  2. 是否存在v-pre,如果存在,则不用解析;
  3. 不能存在 node.hasBindings,当节点有绑定 Vue属性的时候,比如指令,事件等,node.hasBindings 会为 true;
  4. 不能存在 node.if 和 node.for,同样,当 节点有 v-if 或者 v-for 的时候,node.if 或者 node.for 为true;
  5. 节点名称不能是slot 或者 component,因为这两者是要动态编译的,不属于静态范畴;
  6. isPlatformReservedTag(node.tag),isPlatformReservedTag 是用于判断该标签是否是正常的HTML 标签,有什么标签呢?
  7. isDirectChildOfTemplateFor(node) 表明了节点父辈以上所有节点不能是 带有v-for的template;
  8. Object.keys(node).every(isStaticKey),isStaticKey是一个函数,用于判断传入的属性是否在右边的范围内(type,tag,attrsList,attrsMap,plain,parent,children,attrs);
标记静态节点

标记静态根节点

渲染函数的生成

//渲染函数的生成
  res.render = createFunction(compiled.render, fnGenErrors);
  function createFunction (code, errors) {
    try {
      return new Function(code)
    } catch (err) {
      errors.push({ err: err, code: code });
      return noop
    }
  }