Vue源码解析之 编译

946 阅读3分钟

本文为原创文章,未获授权禁止转载,侵权必究!

本篇是 Vue 源码解析系列第 2 篇,关注专栏

前言

Vue.js 提供了 2 个版本,一个是 Runtime + Compiler,一个是 Runtime only,前者包含编译代码,可以把编译过程放在运行时做,后者不包含编译代码,需要借助 webpack 的 vue-loader 事先把模板编译成 render 函数。本文基于 Runtime + Compiler 的 Vue.js,它的入口是src/platforms/web/entry-runtime-with-compiler.js,下面我们来一探究竟。

从入口开始

// src/platforms/web/entry-runtime-with-compiler.js

const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)
  
  // 省略
  const options = this.$options
  // resolve template/el and convert to render function
  if (!options.render) {
    let template = options.template
    if (template) {
      // 省略
    } else if (el) {
      template = getOuterHTML(el) // 返回 "<div id=\"app\">\n {{ msg }}\n </div>"
    }
    if (template) {
      // 省略
      
      const { render, staticRenderFns } = compileToFunctions(template, {
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns

      // 省略
    }
  }
  return mount.call(this, el, hydrating)
}

可以看出这段逻辑,编译的入口就在这里:

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

一探究竟

上文中 compileToFunctions 方法被定义在 src/platforms/web/compiler/index.js

import { baseOptions } from './options'
import { createCompiler } from 'compiler/index'

const { compile, compileToFunctions } = createCompiler(baseOptions)

export { compile, compileToFunctions }

compileToFunctions 实际上是 createCompiler 返回值,它被定义在 src/compiler/index.js

export const createCompiler = createCompilerCreator(function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
  const ast = parse(template.trim(), options)
  if (options.optimize !== false) {
    optimize(ast, options)
  }
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
})

而真正的编译都是在 baseCompile 方法中执行的

function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
  const ast = parse(template.trim(), options)
  if (options.optimize !== false) {
    optimize(ast, options)
  }
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
}

createCompilerCreator 方法被定义在 src/compiler/create-compiler.js ,该方法返回的是一个 createCompiler 函数,最终返回一个对象,包含 compilecompileToFunctions 属性。而 compileToFunctions 实际是入口 $mount 函数中调用的 compileToFunctions 方法。

export function createCompilerCreator (baseCompile: Function): Function {
  return function createCompiler (baseOptions: CompilerOptions) {
    function compile (
      template: string,
      options?: CompilerOptions
    ): CompiledResult {
      const finalOptions = Object.create(baseOptions)
      // 省略
      const compiled = baseCompile(template, finalOptions) //  baseCompile 方法传入
      // 省略
      return compiled
    }

    return {
      compile,
      compileToFunctions: createCompileToFunctionFn(compile) // compile 方法传入
    }
  }
}

之后我们再来查看 createCompileToFunctionFn 方法,它被定义在 src/compiler/to-function.js

export function createCompileToFunctionFn (compile: Function): Function {
  const cache = Object.create(null)

  return function compileToFunctions (
    template: string,
    options?: CompilerOptions,
    vm?: Component
  ): CompiledFunctionResult {
    options = extend({}, options)
    // 省略
    
    // compile  核心
    const compiled = compile(template, options)
    
    // 省略
    
    // turn code into functions
    const res = {}
    const fnGenErrors = []
    res.render = createFunction(compiled.render, fnGenErrors)
    res.staticRenderFns = compiled.staticRenderFns.map(code => {
      return createFunction(code, fnGenErrors)
    })
    
    // 省略
    return (cache[key] = res)
  }
}

至此,我们总算找到了 compileToFunctions 的最终定义,它接收三个参数:编译模板 template,编译配置 options 和 Vue 实例 vm。核心的编译过程就一行代码:

const compiled = compile(template, options)

compile 函数实际执行 createCompileToFunctionFn 时作为参数传入,它被定义在 createCompiler 函数中

function compile (
  template: string,
  options?: CompilerOptions
): CompiledResult {
  const finalOptions = Object.create(baseOptions)
  // 省略
  const compiled = baseCompile(template, finalOptions)
  // 省略
  return compiled
}

compile 函数真正执行编译的是 baseCompile 方法,它在 createCompilerCreator 函数执行时作为参数传入

export const createCompiler = createCompilerCreator(function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
  const ast = parse(template.trim(), options)
  if (options.optimize !== false) {
    optimize(ast, options)
  }
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
})

最后,我们要找的编译入口就是执行了 baseCompile 函数,它主要做了三件事:

  • 解析模板字符串生成AST语法树,parse 函数目的是将 template 模板字符串转换成 AST 语法树,整个解析过程是利用正则表达式对模板的解析。
const ast = parse(template.trim(), options)
  • 优化语法树,optimize 过程是深度遍历 AST 树,去检测它的每一个子树是否为静态节点,如果是,则它们生成 DOM 永远不需要改变。
optimize(ast, options)
  • 生成代码,generate 过程是将 AST 树转换为 code
const code = generate(ast, options)

AST语法树结构

// template 模板
<div id="app">{{ msg }}</div>

// template 转换 AST
{
    attrs: [
        {
            name: "id",
            value: "\"app\""        
        }    
    ],
    attrsList: [
        {
            name: 'id',
            value: 'app'        
        }    
    ],
    attrsMap: { id: 'app' },
    children: [
        {
            expression: "\"\\n    \"+_s(msg)+\"\\n  \"",
            static: false,
            text: "\n    {{ msg }}\n  ",
            tokens: [
                "\n    ",
                {@binding: 'msg'},
                "\n  "
            ],
            type: 2        
        }    
    ],
    parent: undefined,
    plain: false,
    static: false,
    staticRoot: false,
    tag: 'div',
    type: 1
}

总结

Vue 编译实际是执行了 baseCompile 函数,它主要做了三件事:解析模板字符串生成 AST 树、优化语法树、生成代码。而核心的 parse 解析过程是通过一系列的正则表达式顺序解析模板,当解析到开始标签、闭合标签、文本的时候都会分别执行对应的回调函数,来达到构造 AST 树的目的。

参考

Vue.js 技术揭秘

Vue 源码解析系列

  1. Vue源码解析之 源码调试
  2. Vue源码解析之 编译
  3. Vue源码解析之 数据驱动
  4. Vue源码解析之 组件化
  5. Vue源码解析之 合并配置
  6. Vue源码解析之 生命周期
  7. Vue源码解析之 响应式对象
  8. Vue源码解析之 依赖收集
  9. Vue源码解析之 派发更新
  10. Vue源码解析之 nextTick