Vue 源码分析 - 模板编译 - 1(入口分析) (🔥🔥超全解析详细到每一行!!!)

前言

文中讲解代码为 Vue 组合式 API 的最后一个版本 2.6.14 ~ 为了更好的理解,省略了部分代码,留下核心逻辑进行讲解。如需完整代码解释,可在代码库拉取完整代码,每一行均有完整注释,并在不断地完善中,我也会不断补充所有 Vue 源码中涉及到的逻辑;文章中有需要纠正的地方,欢迎大家指出,我们共同打造一份详细易懂的源码解析 ~

github代码地址:欢迎star🌟🌟🌟

gitee代码地址:欢迎star🌟🌟🌟

如果大家想要了解代码中提到的函数,可以评论区留言,我会补充对该函数的讲解!

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

  1. 入口分析:寻找真正的编译入口
  2. 解析阶段:将模板字符串解析为抽象语法树(AST)
  3. 优化阶段:遍历AST,标记静态节点以便后续优化
  4. 生成阶段:将优化后的AST生成渲染函数(render function)

太沉重的篇幅不易于我们理解,这一章我们主要讲解模板编译的入口分析 ~

流程讲解

挂载实例

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

自动调用挂载方法

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

想了解 Vue 初始化流程的,可以看这篇文章了解:new Vue 的过程发生了什么

php 复制代码
// 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方法

php 复制代码
// main.js

new Vue({
  ...
}).$mount('#app'); // 挂载实例

组件实例挂载

组件实例的创建的过程,当执行挂载逻辑时,依旧走的 $mount方法

本章的重点是 Vue 实例化时候模板的编译,所以组件实例的挂载将放在其他文章中重点讲解

swift 复制代码
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 ),而不会进行模板编译

javascript 复制代码
// src\platforms\web\runtime\index.js

Vue.prototype.$mount = function (el, hydrating) { // 挂载
  ...
  return mountComponent(this, el, hydrating)
};

包含编译器的版本 (Runtime+Compiler)

在包含编译器的版本中,Vue 需要处理从模板到渲染函数的编译过程。这需要对 $mount 方法进行扩展

javascript 复制代码
// 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 作为模板字符串

ini 复制代码
// 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 方法调用结果的返回值中解构出来的

javascript 复制代码
// 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 转换为渲染函数代码
scss 复制代码
// 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 方法时候调用的那个创建编译器实例的方法

scss 复制代码
// src\platforms\web\compiler\index.js

var { compile, compileToFunctions } = createCompiler(baseOptions);
php 复制代码
// 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 函数调用的返回结果。因此,我们最后再去了解一下该函数

python 复制代码
// src\compiler\create-compiler.js

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

该函数主要在编译过程中,对错误发出提示信息以及对编译结果进行缓存

该函数的重点是调用了 compile 方法,然后 compile 调用了最核心的编译方法 baseCompile

php 复制代码
// 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 在保持核心功能强大和灵活的同时,提供了良好的开发体验和代码质量

相关推荐
abc80021170341 小时前
前端Bug 修复手册
前端·bug
Best_Liu~2 小时前
el-table实现固定列,及解决固定列导致部分滚动条无法拖动的问题
前端·javascript·vue.js
_斯洛伐克2 小时前
下降npm版本
前端·vue.js
苏十八3 小时前
前端进阶:Vue.js
前端·javascript·vue.js·前端框架·npm·node.js·ecmascript
st紫月4 小时前
用MySQL+node+vue做一个学生信息管理系统(四):制作增加、删除、修改的组件和对应的路由
前端·vue.js·mysql
乐容5 小时前
vue3使用pinia中的actions,需要调用接口的话
前端·javascript·vue.js
似水明俊德5 小时前
ASP.NET Core Blazor 5:Blazor表单和数据
java·前端·javascript·html·asp.net
至天6 小时前
UniApp 中 Web/H5 正确使用反向代理解决跨域问题
前端·uni-app·vue3·vue2·vite·反向代理
与墨学长6 小时前
Rust破界:前端革新与Vite重构的深度透视(中)
开发语言·前端·rust·前端框架·wasm
H-J-L7 小时前
Web基础与HTTP协议
前端·http·php