一、简介

在创建vue实例的过程中,我们可以通过template属性,指定vue实例对应的html结构,或者放到<template></template>标签中,这种html结构我们称之为模版,其中不仅包含普通的html标签,还包含vue提供的插值表达式{{}}、指令等用来获取vue提供的数据,实现数据驱动视图。模版编译的作用就是将我们指定的模版,编译成render()函数,render()函数继而实现模版渲染。模版编译根据执行时机分为两种:
🦋 运行时编译。完整版vue携带模版编译的代码,只不过代码量大,性能比较低
🦋 构建时编译。vue-cli默认使用的是运行时vue,模版编译需要由webpackvue-loader插件完成。
接下来我们来简单的体验一下模版编译生成的效果,测试代码如下:

<div id="demo">
  <h1>app's demo</h1>
  <h2>{{person}}</h2>
</div>
<script>
const vm = new Vue({
  el: '#demo',
  data() {
    return {
      person: 'ls'
    }
  },
  mounted() {
    console.log('mounted')
    console.log(this.$options.render)
  }

})
</script>

控制台输出的render()函数:

(function anonymous() {
  with (this) {
    return _c(
      'div',  // vm实例对应的模版的标签为div
      {attrs: {"id": "demo"}},  // 增加一个属性id
      [  // 子节点序列
        _c('h1', [_v("app's demo")]),  // 创建文本节点并放到h1标签中
        _v(" "),  // 空格文本节点
        _c('h2', [_v(_s(person))])  // 将属性person对应的值转成文本,然后创建文本节点,并放在h2标签中
      ]
    )
  }
})

anonymous表示它是一个匿名函数,with(){}语法用于临时扩展作用域链,严格模式下禁用,花括号内部变量的this都指向with()方法的实参,这样就可以省略对象,直接使用目标对象的属性,这里的this指向的是vmwith()中的_c就是vm._c其中使用到几个函数,在下图中依次对这几个函数进行简单的介绍:
在这里插入图片描述

二、模版编译入口

模版编译的过程高阶函数含量过高,过程比较绕。主要做的事情:
🦋:将平台相关的编译选项和用户传入的配置选项合并
🦋:将模版转化为抽象语法树
🦋:根据抽象语法树获取render函数
🦋:为Vue添加编译方法,根据配置项和render函数完成模版编译
下面是从项目入口文件寻找模版编译核心方法的过程,

  • 项目入口文件是[platforms/web/entry-runtime-with-compiler.ts],其中从[platforms/web/runtime-with-compiler.ts]中引入了Vue的定义;
  • [platforms/web/runtime-with-compiler.ts]中给Vue增加了compile属性,这个属性值指向compileToFunctions,而compileToFunctions是通过import引入的,其定义在[platforms/web/compiler/index.ts]文件中;
  • compileToFunctions是通过结构赋值的形式,通过解析createCompiler(baseOptions)执行的结果解析出来的,其中baseOptions参数是和平台相关的配置项,包括模块、组件等;creatComplie()函数也是通过import,从[compiler/index.ts]中引入的,这个文件中的代码是和平台无关的通用代码。creatComplie()函数是createCompilerCreator(baseCompile)执行返回,参数baseCompile是一个函数,用于将模板转换成AST 抽象语法树;
  • createCompilerCreator()在[compiler/create-compiler.ts]中定义,返回值也是一个函数,这个函数的返回值是一个对象,其中有compile属性和compileToFunctions属性,这两个属性都是方法
  • compile()会将平台相关的选项和平台无关的选项合并,然后调用传进来的参数方法baseCompile(合并后的选项)
    compileToFunctions()会将模版编译为render()函数

在这里插入图片描述
接下来我们来逐个击破,看看这几个函数主要的功能。

三、模版编译过程

(一)createCompileToFunctionFn()compileToFunctions()

首先看createCompileToFunctionFn()方法,这个方法的作用是用来创建compileToFunctions()方法的,compileToFunctions()方法用来将模版编译为render()函数,之所以使用高阶函数的形式返回compileToFunctions()方法,是为了使用闭包对生成的编译结果进行缓存
在这里插入图片描述

(二)compile()

上面createCompileToFunctionFn()方法需要接收compile()方法作为参数,compile()方法用来将模版编译成render()函数,接下来我们看这个方法里面都做了什么。主要三件事:

  • 合并选项
  • 调用baseCompile()进行编译
  • 返回编译结果
    在这里插入图片描述

(二)baseCompile()

baseCompile()是调用createCompilerCreator()方法的时候传递的实参,里面的代码并不多,但是每一步都很复杂
在这里插入图片描述
主要是三个函数,parse()optimize()generate()
先讲一下关键的概念----AST

  • 抽象语法树简称 AST (Abstract Syntax Tree)
  • 使用对象的形式描述树形的代码结构
  • 此处的抽象语法树是用来描述树形结构的HTML字符串

为什么要使用抽象语法树

  • 模板字符串转换成 AST 后,可以通过 AST 对模板做优化处理
  • 标记模板中的静态内容(不依赖数据的内容),在patch(新旧节点对比并更新dom)的时候直接跳过静态内容
  • 在patch的过程中静态内容不需要对比和重新渲染

AST在很多的构建工具中都有使用,比如babel可以将es6转成兼容性更强的es5,就是先将代码转换为AST,然后再进行解析转化为es5。
这里介绍一个查看AST的网站,可以查看各种项目中各种构建工具生成的AST
在这里插入图片描述
抽象语法树虽然听起来比较难理解,但也只是一个普通的对象,我们来看一下生成AST的方法:

function createASTElement(tag, attrs, parent) {
    return {
        type: 1,
        tag,
        attrsList: attrs,
        attrsMap: makeAttrsMap(attrs),
        rawAttrsMap: {},
        parent,
        children: []
    };
}
💙 parse()

parse()将模版转换为AST的过程比较的复杂,这里只介绍大体的工作步骤。
在这里插入图片描述

💙 optimize()

parse()将模版转换为AST之后,会调用optimize()方法优化抽象语法树。这个方法的作用是标记AST中的静态节点,将来在更新DOM的时候就可以直接跳过静态节点。
在这里插入图片描述

💙 generate()

第三步是使用generate()将将AST转换成字符串形式的js代码。generate()方法里面的代码不多

function generate(ast: ASTElement | void,options: CompilerOptions): CodegenResult {
  // 创建CodegenState对象,用来保存生成代码时的一些选项
  const state = new CodegenState(options)
  // 如果ast存在,且ast的tag是script,说明是根节点的script标签,直接返回null
  // 如果ast不存在,说明是纯模版,直接返回_c('div'),_c是createElement的别名
  // 如果ast存在,且ast的tag不是script,说明是根节点的template标签,调用genElement生成代码
  const code = ast
    ? ast.tag === 'script'
      ? 'null'
      : genElement(ast, state)
    : '_c("div")'
  // 返回渲染函数和静态渲染函数
  return {
    render: `with(this){return ${code}}`,
    staticRenderFns: state.staticRenderFns
  }
}

但是理解起来很困难。注释写出来的就不啰嗦了,首先解释state中比较重要的两个属性

  • staticRenderFns用来存储静态根节点的渲染函数,这个属性最终会被generate()函数返回出去
  • pre用来标识当前处理的节点是否允许跳过编译。AST的pre属性在parse()阶段就标记好了,是通过判断标签上有没有v-pre指令。v-pre指令是用来标识当前节点跳过编译过程。可利用它跳过:没有使用指令语法、没有使用插值语法的节点,会加快编译

重点解释genElement()生成代码的过程
在这里插入图片描述
genElement()方法最终会返回code,并且会给state.staticRenderFns添加元素,generate()会返回{code,staticRenderFns}codecompileToFunctions()函数中,通过new Function()形式,由字符串转换为真正的函数。

四、组件化

(一)全局组件

首先回顾一下全局组件的定义方法:

// 定义一个名为 button-counter 的新组件
Vue.component('button-counter', {
  data: function () {
    return {
      count: 0
    }
  },
  template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>'
})

使用的是Vue的静态方法component()注册,注册完就可以在项目中任何一个Vue实例中使用

<button-counter></button-counter>

Vue.component()的初始化在[src/core/index.ts]initGlobalAPI()方法中进行,其中的initAssetRegisters(Vue)事件用来初始化Vue.componentVue.directiveVue.filter静态方法,因为这几个方法的使用格式很类似,都是Vue.xx('xx',xx)的形式。这种定义方法在平时写代码的时候也可以借鉴,但是我感觉好像更麻烦了,因为还需要一个类型知道当前使用的是哪一个方法。首先说一下这几个方法的使用,以component()方法为例,有两种用法,一种是定义组件,一种是获取已经定义的组件。
🌺 定义组件
Vue.component('组件名',options配置对象)
🌺 获取组件
Vue.component('组件名')
如果只有一个参数,说明这个方法是用来获取已经定义的组件的。
下面梳理了Vue.component()方法执行过程中主要做的事情:
在这里插入图片描述

(二)局部组件

先回顾一下局部组件的注册

const ButtonCounter = {
  data: function () {
    return {
      count: 0
    }
  },
  template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>'
}

const vm = new Vue({
  el: '#demo',
  components: {
    'button-counter': ButtonCounter
  }
})

组件在创建Vue实例的时候作为配置选项传递进去。

五、模版解析

上面讲了一下组件的注册过程,下面讲一下模版渲染过程中,遇到组件<button-counter></button-counter>,Vue是怎么解析的。假设模版中的内容有一个根标签和一个组件标签:

<div id="demo">
  <button-counter></button-counter>
</div>

js中的代码和上边一样

const ButtonCounter = {
  data: function () {
    return {
      count: 0
    }
  },
  template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>'
}

const vm = new Vue({
  el: '#demo',
  components: {
    'button-counter': ButtonCounter
  }
})

在执行new Vue()代码时,会创建Vue实例,创建Vue实例的阶段会通过调用vm.$mount()方法进行页面初始化。此时传给new Vue()的配置参数中没有template属性,会先根据el属性获取根节点,将根节点的outerHTML作为template
在这里插入图片描述
并且,此时也没有传递render()函数,就会根据模版生成render()函数:

const { render, staticRenderFns } = compileToFunctions(
  template,
  {
    outputSourceRange: __DEV__,
    shouldDecodeNewlines,
    shouldDecodeNewlinesForHref,
    delimiters: options.delimiters,
    comments: options.comments
  },
  this
)

compileToFunctions()在第三章模版编译过程中有讲到,它会将模版编译成render()函数,返回值是一个对象,其中包含编译好的render()函数和静态渲染函数staticRenderFns(),静态渲染函数用来渲染静态内容,也就是不需要随着数据的改变而更新的内容。此时看一下render()长啥样:

(function anonymous(
) {
with(this){return _c('div',{attrs:{"id":"demo"}},[_c('button-counter')],1)}
})

render()函数表示创建一个根节点,标签为div,这个根节点的id属性为demo,并且有一个子元素,子元素的标签为button-counter,也就是我们自定义的标签。第四个参数是用来表示转化children参数的类型,虚拟节点的children属性可能有嵌套,通过这个参数来判断调用哪个方法来处理嵌套的children。这个值有两种情况,一种是1,表示children是简单的嵌套,只需要简单的处理,不进行嵌套节点的处理;一种是2,当执行用户传入的render()时,这个值就是2,表示需要递归处理children嵌套。
render()函数有两种形式,一种是用户手动传进来,一种是Vue将template转化为render()函数,初始化渲染过程涉及到的步骤,可以通过调试的方法进行详细的查看,我这里通过调试,大致梳理了代码在生成虚拟DOM树的过程中的执行过程。要点也使用文字总结一下:
1️⃣ 初始化过程会初始化渲染相关的函数,例如_c()是当传入模版时用来创建元素的函数、$createElement()是用户传入render时创建元素的函数;它们两个都是对传进来的参数进行处理,最后调用一个公用的方法:createElement()。这个方法是用来生成虚拟节点的,虚拟节点有好几种:浏览器内置标签、自定义组件、未知标签,这个方法内部会根据tag参数判断,生成对应种类的虚拟节点。对于根节点而言,此时生成的虚拟节点,就是虚拟DOM树。
2️⃣ 如果传入的是自定义组件,在createElement()方法中会调用创建组件的方法,创建一个继承Vue构造函数的组件构造函数,并且创建一个自定义组件的虚拟节点。在生成render()函数的过程中,由于子节点是组件,就会配置render()函数的最后一个参数为1,表示在创建虚拟节点的时候进行嵌套节点的处理。

(function anonymous(
) {
with(this){return _c('div',{attrs:{"id":"demo"}},[_c('button-counter')],1)}
})

3️⃣ 如果用户没有传入render配置项,就会根据template或者el生成render函数。此时的render()函数创建虚拟节点使用的都是_c()
4️⃣ 如果用户传入了render配置项,会把vm.$createElement作为h函数传递给render函数,用来生成虚拟节点。此时children可能有嵌套,会进行处理。
5️⃣ 如果用户指定了template,模版中的静态内容会转化为staticRenderFns静态渲染函数的数组,render()函数中会通过_m()调用staticRenderFns中的函数。
在这里插入图片描述

〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️〰️
下面是调试过程中使用的三个示例,分别是自定义render、传入template、传入自定义组件的情况
在这里插入图片描述

Logo

前往低代码交流专区

更多推荐