阅读 54

VUE源码系列六:编译原理

前言

之前的系列我们有分析过模版编译成真实DOM的过程,有一个环节就是把模版编译成了render函数,这个过程我们称作编译
Vue.js 提供了 2 个版本,一个是 Runtime + Compiler 的,一个是 Runtime only 的,前者是包含编译代码的,可以把编译过程放在运行时做,后者是不包含编译代码的,需要借助 webpack 的 vue-loader 事先把模板编译成 render函数;
理解编译过程对理解Vue的指令以及内置组件有更好的帮助,由于编译过程相对复杂,我们只分析整体的流程,不要太扣细节,正所谓成大事者,不拘小节😄;

编译入口

$mount

源码:src/platforms/web/entry-runtime-with-compiler.js

Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
    // ...
      
      /**
       * 编译的入口
       * 将template编译成render函数,staticRenderFns是编译优化,static静态不需要在VNode更新时进行patch,优化性能
       */
      const { render, staticRenderFns } = compileToFunctions(template, {
        outputSourceRange: process.env.NODE_ENV !== 'production',
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      /* 将编译成的render赋值给options.render */
      options.render = render
      options.staticRenderFns = staticRenderFns
  }
  /* 最后执行一开始缓存下来的原型上的mount */
  return mount.call(this, el, hydrating)
}
复制代码

这段代码我们之前分析过,compileToFunctions 方法就是把模板 template 编译生成 render 以及 staticRenderFns
源码: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 方法的返回值,该方法接收一个编译配置参数,接下来我们来看一下 createCompiler 方法的定义。

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
  }
})
复制代码

createCompiler 方法实际上是通过调用 createCompilerCreator 方法返回的,该方法传入的参数是一个函数,真正的编译过程都在这个 baseCompile 函数里执行,baseCompile我们后边分析,那么 createCompilerCreator 又是什么呢

createCompilerCreator

它的定义在 src/compiler/create-compiler.js 中

export function createCompilerCreator (baseCompile: Function): Function {
  /*
  * createCompiler主要做了两件事:
  * 1. 合并options,将平台自有的option与传入的option进行合并
  * 2. baseCompile,进行模版的基础编译
  */
  return function createCompiler (baseOptions: CompilerOptions) {
    /*编译,将模板template编译成AST、render函数以及staticRenderFns函数*/
    function compile (
      template: string,
      options?: CompilerOptions
    ): CompiledResult {
      const finalOptions = Object.create(baseOptions)
      const errors = []
      const tips = []

      if (options) {
        /*做下面这些merge的目的因为不同平台可以提供自己本身平台的一个baseOptions,
        内部封装了平台自己的实现,然后把共同的部分抽离开来放在这层compiler中,
        所以在这里需要merge一下*/
        if (options.modules) {
          finalOptions.modules =
            (baseOptions.modules || []).concat(options.modules)
        }
        /* 合并指令 */
        if (options.directives) {
          finalOptions.directives = extend(
            Object.create(baseOptions.directives || null),
            options.directives
          )
        }
        
        for (const key in options) {
          /*合并其余的options,modules与directives已经在上面做了特殊处理了*/
          if (key !== 'modules' && key !== 'directives') {
            finalOptions[key] = options[key]
          }
        }
      }

      /*基础模板编译,得到编译结果*/
      const compiled = baseCompile(template.trim(), finalOptions)
      compiled.errors = errors
      compiled.tips = tips
      return compiled
    }

    return {
      compile,
      compileToFunctions: createCompileToFunctionFn(compile)
    }
  }
}
复制代码

可以看到createCompilerCreator返回了一个 createCompiler 的函数,它接收一个 baseOptions 的参数,返回的是一个对象,包括 compile 方法属性和 compileToFunctions 属性,这个 compileToFunctions 对应的就是 $mount 函数调用的 compileToFunctions 方法,它是调用 createCompileToFunctionFn 方法的返回值,我们接下来看一下 createCompileToFunctionFn 方法。

createCompileToFunctionFn

它的定义在 src/compiler/to-function/js 中:

export function createCompileToFunctionFn (compile: Function): Function {
  /* 闭包内的缓存器 */
  const cache = Object.create(null)
  /*带缓存的编译器,同时staticRenderFns以及render函数会被转换成Funtion对象*/
  return function compileToFunctions (
    template: string,
    options?: CompilerOptions,
    vm?: Component
  ): CompiledFunctionResult {
    options = extend({}, options)

    const key = options.delimiters
      ? String(options.delimiters) + template
      : template
    /* 取缓存 */
    if (cache[key]) {
      return cache[key]
    }

    /* 编译的核心 */
    const compiled = compile(template, options)

    /*将render转换成Funtion对象*/
    res.render = createFunction(compiled.render, fnGenErrors)
    /*将staticRenderFns全部转化成Funtion对象 */
    res.staticRenderFns = compiled.staticRenderFns.map(code => {
      return createFunction(code, fnGenErrors)
    })

    /*存放在缓存中,以免每次都重新编译*/
    return (cache[key] = res)
  }
}
复制代码

可以看到,我们创建了一个在闭包内的空对象,每次将之前的编译结果缓存起来,下次再进来就会先取缓存,避免每次取重新编译。
至此我们总算找到了 compileToFunctions 的最终定义,它接收 3 个参数、编译模板 template,编译配置 options 和 Vue 实例 vm。核心的编译过程就一行代码:

const compiled = compile(template, options)
复制代码

我们上边已经贴出了compile的源码,compile 函数执行的逻辑是先处理配置参数,真正执行编译过程就一行代码:

const compiled = baseCompile(template, finalOptions)
复制代码

因此,兜兜转转我们又回到了baseCompile,它也正是编译的真正入口

baseCompile

源码:src/compiler/index.js

export const createCompiler = createCompilerCreator(function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
  /* 解析模板字符串生成 AST */
  const ast = parse(template.trim(), options)
  if (options.optimize !== false) {
    /**
     * 优化语法树
     * 优化的目标:生成模版AST,检测不需要进行DOM改变的静态子树,一🥚检测到这些静态子树,我们就能做以下事情:
     * 1.把他们变成常数, 这样就不需要每次重新渲染的时候创建新的节点
     * 2.在patch过程中直接跳过
     */
    /* optimize主要作用是标记static静态节点,当更新的时候,会直接跳过静态节点,性能优化 */
    optimize(ast, options)
  }
  /**
   * 将AST转化成render funtion字符串的过程
   * 根据AST生成所需的code,内部包含render与staticRenderFns
   */
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
})
复制代码

它主要做了三件事
1.解析模板字符串生成 AST

const ast = parse(template.trim(), options)
复制代码

2.优化语法树

optimize(ast, options)
复制代码

3.生成代码

const code = generate(ast, options)
复制代码

小结:编译入口逻辑之所以这么绕,是因为 Vue.js 在不同的平台下都会有编译的过程,因此编译过程中的依赖的配置 baseOptions 会有所不同。而编译过程会多次执行,但这同一个平台下每一次的编译过程配置又是相同的,为了不让这些配置在每次编译过程都通过参数传入,Vue.js 利用了函数柯里化的技巧很好的实现了 baseOptions 的参数保留。同样,Vue.js 也是利用函数柯里化技巧把基础的编译过程函数抽出来,通过 createCompilerCreator(baseCompile) 的方式把真正编译的过程和其它逻辑如对编译配置处理、缓存处理等剥离开,这样的设计还是非常巧妙的。

接下来我们分别解析parse、optimize、generate

parse

编译过程首先就是对模板做解析,生成 AST,它是一种抽象语法树,是对源代码的抽象语法结构的树状表现形式。在很多编译技术中,如 babel 编译 ES6 的代码都会先生成 AST。
这个过程是比较复杂的,它会用到大量正则表达式对字符串解析。 举个🌰子:

<ul :class="bindCls" class="list" v-if="isShow">
    <li v-for="(item,index) in data" @click="clickItem(index)">{{item}}:{{index}}</li>
</ul>
复制代码

经过 parse 过程后,生成的 AST 如下:

ast = {
  'type': 1, // 节点类型
  'tag': 'ul', // 标签名
  'attrsList': [], // 属性列表
  'attrsMap': {
    ':class': 'bindCls',
    'class': 'list',
    'v-if': 'isShow'
  },
  'if': 'isShow', // 指令
  'ifConditions': [{
    'exp': 'isShow',
    'block': // ul ast element
  }],
  'parent': undefined, // 父子关系
  'plain': false,
  'staticClass': 'list', // 静态class
  'classBinding': 'bindCls',
  'children': [{  // 子节点
    'type': 1,
    'tag': 'li',
    'attrsList': [{
      'name': '@click',
      'value': 'clickItem(index)'
    }],
    'attrsMap': {
      '@click': 'clickItem(index)',
      'v-for': '(item,index) in data'
     },
    'parent': // ul ast element
    'plain': false,
    'events': {
      'click': {
        'value': 'clickItem(index)'
      }
    },
    'hasBindings': true,
    'for': 'data',
    'alias': 'item',
    'iterator1': 'index',
    'children': [
      'type': 2,
      'expression': '_s(item)+":"+_s(index)'
      'text': '{{item}}:{{index}}',
      'tokens': [
        {'@binding':'item'},
        ':',
        {'@binding':'index'}
      ]
    ]
  }]
}
复制代码

可以看到,生成的 AST 是一个树状结构,每一个节点都是一个 ast element,除了它自身的一些属性,还维护了它的父子关系,如 parent 指向它的父节点,children 指向它的所有子节点。

parse 的目标是把 template 模板字符串转换成 AST 树,它是一种用 JavaScript 对象的形式来描述整个模板。那么整个 parse 的过程是利用正则表达式顺序解析模板,来达到构造 AST 树的目的。

AST 元素节点总共有 3 种类型,type 为 1 表示是普通元素,为 2 表示是表达式,为 3 表示是纯文本。

当 AST 树构造完毕,下一步就是 optimize 优化这颗树。

optimize

optimize就是对AST这棵树做优化,那么为什么要有优化过程,因为我们知道 Vue 是数据驱动,是响应式的,但是我们的模板并不是所有数据都是响应式的,也有很多数据是首次渲染后就永远不会变化的,那么这部分数据生成的 DOM 也不会变化,我们可以在 patch 的过程跳过对他们的比对。

optimize的作用就是深度遍历这个 AST 树,去检测它的每一颗子树是不是静态节点,如果是静态节点就将相应节点做静态标志,比如我们上边的例子经过optimize函数优化后变成了这样:

ast = {
  'type': 1,
  'tag': 'ul',
  'attrsList': [],
  'attrsMap': {
    ':class': 'bindCls',
    'class': 'list',
    'v-if': 'isShow'
  },
  'if': 'isShow',
  'ifConditions': [{
    'exp': 'isShow',
    'block': // ul ast element
  }],
  'parent': undefined,
  'plain': false,
  'staticClass': 'list',
  'classBinding': 'bindCls',
  'static': false,
  'staticRoot': false,
  'children': [{
    'type': 1,
    'tag': 'li',
    'attrsList': [{
      'name': '@click',
      'value': 'clickItem(index)'
    }],
    'attrsMap': {
      '@click': 'clickItem(index)',
      'v-for': '(item,index) in data'
     },
    'parent': // ul ast element
    'plain': false,
    'events': {
      'click': {
        'value': 'clickItem(index)'
      }
    },
    'hasBindings': true,
    'for': 'data',
    'alias': 'item',
    'iterator1': 'index',
    'static': false,
    'staticRoot': false,
    'children': [
      'type': 2,
      'expression': '_s(item)+":"+_s(index)'
      'text': '{{item}}:{{index}}',
      'tokens': [
        {'@binding':'item'},
        ':',
        {'@binding':'index'}
      ],
      'static': false
    ]
  }]
}
复制代码

可以看到每个节点又多了static属性,我们通过 optimize 我们把整个 AST 树中的每一个 AST 元素节点标记了 static 和 staticRoot(静态根节点),它会影响我们接下来执行代码生成的过程。

generate

编译的最后一步就是把优化后的 AST 树转换成可执行的代码

/**
* 将AST转化成render funtion字符串的过程
* 根据AST生成所需的code,内部包含render与staticRenderFns
*/
const code = generate(ast, options)
复制代码

接着上边的示例,经过编译,生成的render如下:

with(this){
  return (isShow) ?
    _c('ul', {
        staticClass: "list",
        class: bindCls
      },
      _l((data), function(item, index) {
        return _c('li', {
          on: {
            "click": function($event) {
              clickItem(index)
            }
          }
        },
        [_v(_s(item) + ":" + _s(index))])
      })
    ) : _e()
}
复制代码

这里的 _c 函数就是系列一(juejin.im/post/5e06b4…)讲到的createElement,作用是创建虚拟VNode,定义在 src/core/instance/render.js 中。

vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
复制代码

而 _l、_v 定义在 src/core/instance/render-helpers/index.js 中:

export function installRenderHelpers (target: any) {
  target._o = markOnce
  target._n = toNumber
  target._s = toString
  target._l = renderList
  target._t = renderSlot
  target._q = looseEqual
  target._i = looseIndexOf
  target._m = renderStatic
  target._f = resolveFilter
  target._k = checkKeyCodes
  target._b = bindObjectProps
  target._v = createTextVNode
  target._e = createEmptyVNode
  target._u = resolveScopedSlots
  target._g = bindObjectListeners
}
复制代码

顾名思义,_c 就是执行 createElement 去创建 VNode,而 _l 对应 renderList 渲染列表;_v 对应 createTextVNode 创建文本 VNode;_e 对于 createEmptyVNode创建空的 VNode。看一下generate
源码: src/compiler/codegen/index.js

export function generate (
  ast: ASTElement | void,
  options: CompilerOptions
): CodegenResult {
  const state = new CodegenState(options)
  const code = ast ? genElement(ast, state) : '_c("div")'
  return {
    render: `with(this){return ${code}}`,
    staticRenderFns: state.staticRenderFns
  }
}
复制代码

generate 函数首先通过 genElement(ast, state) 生成 code,再把 code 用 with(this){return ${code}}} 包裹起来。先来看一下 genElement

export function genElement (el: ASTElement, state: CodegenState): string {
  if (el.staticRoot && !el.staticProcessed) {
    return genStatic(el, state)
  } else if (el.once && !el.onceProcessed) {
    return genOnce(el, state)
  } else if (el.for && !el.forProcessed) {
    return genFor(el, state)
  } else if (el.if && !el.ifProcessed) {
    return genIf(el, state)
  } else if (el.tag === 'template' && !el.slotTarget) {
    return genChildren(el, state) || 'void 0'
  } else if (el.tag === 'slot') {
    return genSlot(el, state)
  } else {
    // component or element
    let code
    if (el.component) {
      code = genComponent(el.component, el, state)
    } else {
      const data = el.plain ? undefined : genData(el, state)

      const children = el.inlineTemplate ? null : genChildren(el, state, true)
      code = `_c('${el.tag}'${
        data ? `,${data}` : '' // data
      }${
        children ? `,${children}` : '' // children
      })`
    }
    // module transforms
    for (let i = 0; i < state.transforms.length; i++) {
      code = state.transforms[i](el, code)
    }
    return code
  }
}
复制代码

基本就是判断当前 AST 元素节点的属性执行不同的代码生成函数,最后都是返回拼接好的code,有兴趣的可自行去查看每个方法的实现,这里就不一一详细介绍了;

总结

通过parse将模版template生成AST语法树,然后经过optimize方法的优化,标记了一些非响应式的静态节点,在patch的时候可跳过这些标记好的静态属性,优化算法,最后通过generate生成最终的code码,创建生成最终的标签节点。

关注下面的标签,发现更多相似文章
评论