vue源码之模板编译和组件化

1,223 阅读6分钟

模板编译的作用

用户只需要编写类似html的代码-vuejs模板,通过编译器将模板转换为返回VNode的render函数,用户就不用写复杂的render函数了

.vue文件会被webpack在构建的过程中转换成render函数,需要vueloader

tips
  • 在使用vue2的时候,标签中的文本内容尽量不要添加多余的空白和换行的内容,vue2的render函数会全部保持原样输出,这样会额外的增加内存的消耗,而vue3优化了这个问题

模板编译的入口

关于compileToFunctions函数

在我们的入口文件entry-runtime-with-compiler.js中我们可以找到一个compileToFunctions函数

  • compileToFunctions函数的作用就是把template编译成render, staticRenderFns,并返回
  • compileToFunctions核心就是先去找缓存中编译的结果,如果有的话直接返回,没有的话开始编译,并且把编译的字符串形式的代码转换成函数的形式,最后缓存并且返回
    1. 读取缓存中的 CompiledFunctionResult 对象,如果有直接返回

    2. 把模板编译为编译对象(render, staticRenderFns),字符串形式的js代码

      const compiled = compile(template, options)
      ...
      
    3. 调用createFunction把字符串形式的代码转换成js方法

    4. 缓存并返回res对象(render, staticRenderFns方法)

关于createCompiler(baseOptions)函数
createCompiler(baseOptions)函数返回了compileToFunctions函数
  • baseOptions是平台相关的options,src\platforms\web\compiler\options.js 中定义
  • 这个函数中定义了compile函数,而compile函数的核心作用就是合并选项,调用 baseCompile 进行编译、记录错误,最后返回编译好的对象
  • 返回 { compile, compileToFunctions }
    return {
      compile,
      compileToFunctions: createCompileToFunctionFn(compile)
    }
    
  • compileToFunctions函数是模板编译的入口
关于createCompilerCreator(function baseCompile ())函数
createCompilerCreator(function baseCompile ())函数返回了createCompiler函数
  • createCompilerCreator 以baseCompile函数为参数
  • baseCompile函数内部做了一些关于抽象语法树ast相关的事情
    • 把模板转换成ast 抽象语法树---解析parse
    • 优化抽象语法树 --- 优化optimize
    • 把抽象语法树转换成字符串形式的js代码 --- 生成generate
    • 返回 ast/render(渲染函数)/staticRenderFns(静态渲染函数, 生成静态 VNode 树)
  • 返回createCompiler函数
模板转换成ast抽象语法树之parse

parse函数在处理的过程中会调用parseHTML方法,依次去遍历HTML模板字符串,把HTML模板字符串转换成AST对象,类似于一个普通的对象,html中的属性和指令都会记录在ast对象的相应属性上

优化抽象语法树之optimize

优化的目的是用来标记抽象语法树中的静态节点,标记之后将来就不需要重新渲染,在patch的时候可以直接跳过这些静态子树

静态节点:parts of DOM that never needs to change

静态根节点:标签中包含子标签,并且没有动态内容,也就是里面都是纯文本内容,如果标签中只包含纯文本内容,没有子标签,vue中是不会去优化的

把抽象语法树转换成字符串形式的js代码之generate

。。。

模板编译的过程

模板编译就是最终把模板字符串转换成渲染函数

是把模板字符串首先转换成AST对象,然后优化AST对象,优化的过程其实就是在标记静态节点和静态根节点,然后把优化好的ast对象转换成字符串形式的代码,最终把字符串形式的代码通过new Function()转换成匿名函数,这个匿名函数就是最后生成的render函数

总结
  1. 首先来看模板编译的入口函数compileToFunctions
    • 首先从缓存中加载编译好的render函数
    • 如果缓存中没有的话,调用compile(template, options)开始编译
  2. compile函数中(核心是合并options)
    • 首先需要合并baseOptions和传进来的options
    • 然后调用baseCompile(template.trim(), finalOptions)去编译模板
  3. 真正的处理是在baseCompile中完成的,完成了模板编译最核心的三件事情
    • 模板转换成ast抽象语法树之parse
      • parse() --- 把template转换成AST语法树
    • 优化抽象语法树之optimize
      • 标记AST语法树中的静态节点和静态根节点
      • patch()的过程中会跳过这些静态节点,不需要每次重新渲染的时候重新生成节点
    • 优化过的ast对象转换成字符串形式的代码之generate
  4. baseCompile执行完毕之后,会回到compileToFunctions
    • 继续把生成的字符串形式的js代码转变成函数的形式,通过调用createFunction
    • createFunction的底层调用的是new Function()
    • 最后缓存并返回
      // 4. 缓存并返回res对象(render, staticRenderFns方法)
      return (cache[key] = res)
      
    • 调用完compileToFunctions,render和staticRenderFns就初始化完毕了,最终会被挂载到Vue实例的options对应的属性中

模板编译的过程中会标记静态根节点,对静态根节点进行优化处理,重新渲染的时候不需要再处理静态根节点,因为内容不会发生改变 模板中不要写过多的无意义的空白和换行,生成ast对象的时候会保留这些空白和换行,都会被存到内存中,对浏览器渲染没有任何意义

组件化回顾

Vue的核心组成就是数据绑定和组件化

  • 一个Vue组件就是一个拥有预定义选项的一个vue实例
  • 一个组件可以组成页面上的一个功能完备的区域,组件可以包含脚本、样式、模板

组件注册

组件注册方式
  • 全局组件Vue.components内部的实现是通过Vue.extend来实现的

  • 局部组件

Vue.extend(extendOptions)实现

基于传入的选项对象创建了组件的构造函数,组件的构造函数通过原型继承继承自Vue的构造函数,所以组件对象拥有和Vue实例一样的成员,核心代码如下

    const Sub = function VueComponent (options) {
      // 调用 _init() 初始化
      this._init(options)
    }
    // 原型继承自Vue  vue的原型上之前注入了_init(),所以sub中可以访问_init(),所以可以在VueComponent中访问_init()方法
    Sub.prototype = Object.create(Super.prototype)  // 所有的组件都是继承自 vue  super就是vue  
    Sub.prototype.constructor = Sub
    Sub.cid = cid++ // 缓存要用
    Sub.options = mergeOptions(
      Super.options,
      extendOptions
    )
    Sub['super'] = Super

组件的创建过程

回顾首次渲染过程
  • 首先调用Vue构造函数,在构造函数中调用了this._init()
  • this._init()中最终调用了this.$mount()去挂载
  • this.$mount()中调用了mountComponent()
  • mountComponent中又去创建了渲染Watchernew Watcher()
  • 创建了渲染Watchernew Watcher()的时候,传递了一个参数updateComponent
  • updateComponent中最终调用了vm._updatevm._render()
updateComponent = () => {
     vm._update(vm._render(), hydrating)  // _render生成虚拟dom   _update调用patch  对比两个虚拟dom的差异并更新
   }
  • vm._render()中调用了渲染函数,用户传入的或者编译生成的,在渲染函数中通过createElement()创建了Vnode对象

createElement()最终调用了createComponent()

  • createComponent()把组件转换成了Vnode对象
  • 这个函数内部通过installComponentHooks()->componentVNodeHooks初始化了四个钩子函数,并在init钩子函数中创建了组件对象(组件实例)
  • 而init()钩子函数是在patch()的过程中调用的
组件创建和挂载

创建由父到子,挂载由子到父