模板编译之入口分析

Vue 是一个渐进式 JavaScript 框架,提供了简单易用的模板语法,帮助开发者以声明式的方式构建用户界面。Vue 的模板编译原理是其核心之一,它将模板字符串编译成渲染函数,并在运行时高效地更新 DOM。本文将深入探讨 Vue 模板编译的原理和过程编译的过程,也就是解析出 render 函数的过程,该过程分为四个阶段:

  1. 入口分析:寻找真正的编译入口
  2. 解析阶段:将模板字符串解析为抽象语法树(AST)
  3. 优化阶段:遍历AST,标记静态节点以便后续优化
  4. 生成阶段:将优化后的AST生成渲染函数(render function)
这篇文章我们只分享模板编译的入口分析
流程讲解
  • 挂载实例

    在整个 Vue 源码设计中,共有三处地方会执行挂载

    • 自动调用挂载方法

      首先 Vue 在实例化的时候,当传入 el 配置项,Vue 在初始化方法中会自动调用挂载实例方法

      // main.js
      
      new Vue({
        el: '#app'
      });
      
      // src\core\instance\init.js
      
      Vue.prototype._init = function (options) {
      
        ...
        
        if (vm.$options.el) { // 挂载实例
          vm.$mount(vm.$options.el);
        }
      };
      
    • 手动调用挂载方法

      如果 Vue 实例在实例化时没有收到 el 选项,则它处于“未挂载”状态或者我们也可以选择手动挂载,调用 Vue 对外暴露的 $mount方法

      // main.js
      
      new Vue({
        ...
      }).$mount('#app'); // 挂载实例
      
    • 组件实例挂载

      组件实例的创建的过程,当执行挂载逻辑时,依旧走的 $mount方法,本章的重点是 Vue 实例化时候模板的编译。

      var componentVNodeHooks = { // 初始化钩子函数 (在组件的虚拟节点被创建时调用)
        init: function init (vnode, hydrating) {
      
          ...
          
          child.$mount(hydrating ? vnode.elm : undefined, hydrating); // 挂载组件实例
        }
      }
      
    总之不论是哪种方法,最终都会带着 el 属性走到 Vue 原型上的 $mount 方法
  • $mount 方法

    我们紧接着看 $mount 方法,他的主要作用就是将传入的元素( el )或模板( template)编译为渲染函数( render ),下面我们详细展开

    $mount 的不同版本

    • **运行时版本 ( Runtime-Only )**在纯运行时版本中,Vue 依赖于预编译好的渲染函数(render ),而不会进行模板编译

      // src\platforms\web\runtime\index.js
      
      Vue.prototype.$mount = function (el, hydrating) { // 挂载
        ...
        return mountComponent(this, el, hydrating)
      };
      
    • **包含编译器的版本 (Runtime+Compiler)**在包含编译器的版本中,Vue 需要处理从模板到渲染函数的编译过程。这需要对 $mount 方法进行扩展

      // src\platforms\web\entry-runtime-with-compiler.js
      
      var mount = Vue.prototype.$mount; // 备份原始 $mount 方法
      
      Vue.prototype.$mount = function (el, hydrating) {
      
        /* 模板编译 */
        
        return mount.call(this, el, hydrating)
      };
      
    为什么需要两次定义 $mount 方法 ?
    • 模块化设计Vue 的设计是模块化的,基础的 $mount 方法在运行时版本和包含编译器的版本中都存在,它们共享一个基础实现
    • 扩展功能:运行时版本中的 $mount 方法假设已经有了渲染函数,因此直接进行挂载;包含编译器的版本需要在挂载之前进行模板编译,因此需要扩展基础的 $mount 方法
    • 分离关注点:基础的 $mount 方法专注于组件实例的挂载逻辑,扩展的 $mount 方法处理模板编译的额外逻辑,从而在不同场景下提供合适的功能
    获取需要编译的模板

    Vue 的官方提供的生命周期图示,也描述了这一过程

    在这里插入图片描述

    1. 判断 render 选项

    首先,判断如果选项中传入了 render 函数,则直接调用初始定义的 $mount 方法去进行实例挂载 因为我们的初始目的就是为了将 eltemplate 转化为为 render 函数,这就是为何直接传入函数的方式可以提高渲染效率,原因在于:

    • 避免了运行时的模板编译步骤
    • 提供了更灵活和高效的渲染控制
    • 减少了运行时的计算开销
    1. 判断 template 选项

    然后,判断如果选项中传入了 template 选项,则获取对应的 HTML 模板字符串 获取规则:

    • 如果该字符串以 #开头,它将被用作 querySelector 的选择器,并使用所选中元素的 innerHTML 作为模板字符串
    • 如果是 DOM 元素,直接使用元素的 innerHTML 作为模板字符串
    1. 判断 el 选项

      最后,判断如果选项中传入了 el 选项,则获取元素的 innerHTML 作为模板字符串

    // src\platforms\web\runtime\index.js
    
    Vue.prototype.$mount = function (el, hydrating) { // 挂载
      ...
      return mountComponent(this, el, hydrating)
    };
    
    ...
    
    // src\platforms\web\entry-runtime-with-compiler.js
    
    var mount = Vue.prototype.$mount; // 备份原始 $mount 方法
    
    Vue.prototype.$mount = function (el, hydrating) {
      el = el && query(el);
    
      var options = this.$options; // 选项
    
      // 1. 判断 render 选项
      if (!options.render) {
        var template = options.template;
    
        // 2. 判断 template 选项
        if (template) {
          // 如果是字符串, 通过 id获取元素并获取 innerHTML作为模板字符串
          if (typeof template === 'string') {
            template = idToTemplate(template);
          // 如果是元素, 直接获取元素的 innerHTML作为模板字符串
          } else if (template.nodeType) {
            template = template.innerHTML;
          }
          
        // 3. 判断 el 选项
        } else if (el) {
          template = getOuterHTML(el);
        }
    
        // 模板编译
        const { render, staticRenderFns } = compileToFunctions(template, {
          ...
        }, this);
        
        options.render = render; // 渲染函数
        options.staticRenderFns = staticRenderFns; // 静态渲染函数数组
      }
      
      return mount.call(this, el, hydrating) // 挂载
    };
    

    模板编译

    当获取到需要编译的模板后,会调用 compileToFunctions 方法将模板编译成渲染函数和静态渲染函数 本文的重点是模板编译的入口分析,因此,接下来将继续分析 compileToFunctions 函数

    compileToFunctions 方法

    compileToFunctionsVue 中用于将模板字符串编译为渲染函数的关键方法。这个方法的实现涉及多个步骤,包括解析模板、优化生成的抽象语法树 ( AST ),以及生成最终的渲染函数。下面我们逐步解析 compileToFunctions 的实现过程

    compileToFunctions 方法

    首先调用 createCompiler 方法,传入基础编译选项 baseOptions,创建一个编译器实例,然后compileToFunctions 方法是从 createCompiler 方法调用结果的返回值中解构出来的

    // src\platforms\web\compiler\options.js
    
    // 编译器的基础选项
    var baseOptions = {
      expectHTML: true, // 表示预期输入的模板是否为 HTML
      modules: modules$1, // 用于处理特定的功能或特性的模块数组 (class/style/v-model)
      directives: directives$1, // 对特殊指令的处理函数 (v-model/v-text/v-html)
      isPreTag: isPreTag, // 用于判断是否为 <pre> 标签
      isUnaryTag: isUnaryTag, // 用于判断是否为自闭合标签
      mustUseProp: mustUseProp, // 用于判断在给定的标签上绑定属性是否必须使用 prop 进行绑定
      canBeLeftOpenTag: canBeLeftOpenTag, // 用于判断给定的标签是否可以不闭合
      isReservedTag: isReservedTag, // 用于判断是否是平台保留标签
      getTagNamespace: getTagNamespace, // 用于获取标签的命名空间
      staticKeys: genStaticKeys(modules$1) // 用于生成静态键的列表 (优化渲染性能)
    };
    
    ...
    
    // src\platforms\web\compiler\index.js
    
    var { compile, compileToFunctions } = createCompiler(baseOptions);
    

    createCompiler 方法

    紧接着看 createCompiler 方法的定义 我们发现 createCompiler 方法又是从 createCompilerCreator 方法调用结果的返回值中解构出来的,并且传入了 baseCompile 函数作为参数,而该函数便是模板编译中最核心最重要的编译方法,它通过下列三个步骤完成了模板从字符串到渲染函数的转换过程

    • parse(解析):将模板字符串解析为 AST
    • optimize(优化):标记 AST 中的静态节点,减少运行时需要处理的动态节点
    • generate(生成):将优化后的 AST 转换为渲染函数代码
    // src\compiler\index.js
    
    var createCompiler = createCompilerCreator(function baseCompile (template, options) {
      // 将模板字符串解析为 AST
      var ast = parse(template.trim(), options);
      // 对 AST 进行优化
      optimize(ast, options);
      // 将 AST 转换为渲染函数代码
      var code = generate(ast, options);
      
      return {
        ast: ast,
        render: code.render,
        staticRenderFns: code.staticRenderFns
      }
    });
    

    createCompilerCreator 方法

    然后继续看 createCompilerCreator 方法做了哪些事情 通过观察代码,我们知道了 createCompilerCreator 方法是创建编译器的工厂函数 调用 createCompilerCreator 方法并传入最核心的编译方法 baseCompile 后返回 createCompiler 函数,而该函数便是在获取 compile 以及 compileToFunctions 方法时候调用的那个创建编译器实例的方法

    // src\platforms\web\compiler\index.js
    
    var { compile, compileToFunctions } = createCompiler(baseOptions);
    
    // src\compiler\create-compiler.js
    
    function createCompilerCreator (baseCompile) {
      return function createCompiler (baseOptions) { // 创建编译器实例
        function compile (template, options) {
          // 编译模板
          var compiled = baseCompile(template.trim(), finalOptions);
    
          return compiled
        }
    
        return {
          // 将模板字符串编译为渲染函数代码
          compile: compile,
          // 将模板字符串编译为渲染函数
          compileToFunctions: createCompileToFunctionFn(compile)
        }
      }
    }
    

    createCompileToFunctionFn 方法

    createCompiler 函数的调用结果中返回 compileToFunctions 函数的时候,我们发现 compileToFunctions 函数是 createCompileToFunctionFn 函数调用的返回结果。因此,我们最后再去了解一下该函数

    // src\compiler\create-compiler.js
    
    return {
      compile: compile,
      compileToFunctions: createCompileToFunctionFn(compile)
    }
    

    该函数主要在编译过程中,对错误发出提示信息以及对编译结果进行缓存 该函数的重点是调用了 compile 方法,然后 compile 调用了最核心的编译方法 baseCompile

    // src\compiler\to-function.js
    
    function createCompileToFunctionFn (compile) {
      return function compileToFunctions (template, options, vm) {
    
        var compiled = compile(template, options); // 编译模板
    
        ...
    
        return (cache[key] = res) // 返回并缓存编译结果
      }
    }
    

总结

Vue 编译入口的逻辑之所以这么复杂,采用高阶函数和工厂函数的模式。是因为 Vue 需要在不同的平台下编译,接受不同的配置和选项,并生成适应不同需求的编译器实例,实现高度的灵活性、模块化、可扩展性和性能优化。同时通过缓存机制提高了运行时性能。通过这种方式,Vue 在保持核心功能强大和灵活的同时,提供了良好的开发体验和代码质量

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值