Vue2源码阅读——模板编译(一)

307 阅读4分钟

当我们初始化Vue的时候,我们有如下几种方式:

  1. 第一种
new Vue({
  data () {
    return {
      name:  '请叫我张先森'
    }
  },
  el: '#app'
})
  1. 第二种
new Vue({
  data () {
    return {
      name:  '请叫我张先森'
    }
  },
  el: '#app',
  template: '<div>{{ name }}</div>'
})
  1. 第三种
new Vue({
  data () {
    return {
      name:  '请叫我张先森'
    }
  },
  el: '#app',
  render (h) {
    return h('div', null, [this.name])
  }
})

那么,Vue是如何处理这些不同情况呢? 这就涉及到 Vue挂载中的模板编译, 今天我们就看看 Vue是如何编译模板的;

源码文件定位: src/platforms/web/entry-runtime-with-compiler.js

先提供一个流程图,然后再 进行源码解析

graph TD
queryEl(获取option.el)
queryEl -- 存在 --> checkEl(是否是body或者html) -- 是 --> 抛出异常
checkEl(是否是body或者html) -- 否 --> checkRender
queryEl -- 不存在 --> checkRender(检测是否有render函数)
checkRender -- 有 --> mount(挂载)
checkRender -- 否 --> checkTemplate(检测是否有template选项)
checkTemplate -- 是 --> classifyTemplate(判断template) 
classifyTemplate -- 字符串以 # 开头 --> idToTemplate(获取id的节点的innerHTML) --> compileToFunction(转成render函数)
classifyTemplate -- 元素节点 
--> compileToFunction(转成render函数)
classifyTemplate -- 否则 --> 报错
checkTemplate -- 否 --> checkIsEl(检测el是否存在)
checkIsEl -- 是 --> compileToFunction(转成render函数)
compileToFunction --> mount(挂载)

从上吗的流程图我们可以看到:

  1. 如果 optionrender函数, 那么直接去挂载
  2. 如果 是合法的 template 或者 el 那么将模板编译成 render函数 ,再去挂载 上面所有的工作 都是为了 得到 render函数

下面是源码: 各位读者可以对照源码,再去理解一下 上面的流程图

const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)

  /* istanbul ignore if */
  if (el === document.body || el === document.documentElement) {
    process.env.NODE_ENV !== 'production' && warn(
      `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
    )
    return this
  }

  const options = this.$options
  // resolve template/el and convert to render function
  if (!options.render) {
    let template = options.template
    if (template) {
      if (typeof template === 'string') {
        if (template.charAt(0) === '#') {
          template = idToTemplate(template)
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && !template) {
            warn(
              `Template element not found or is empty: ${options.template}`,
              this
            )
          }
        }
      } else if (template.nodeType) {
        template = template.innerHTML
      } else {
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    } else if (el) {
      template = getOuterHTML(el)
    }
    if (template) {
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile')
      }

      const { render, staticRenderFns } = compileToFunctions(template, {
        outputSourceRange: process.env.NODE_ENV !== 'production',
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref, //
        delimiters: options.delimiters, // 分隔符
        comments: options.comments // 是否保留注释
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns

      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile end')
        measure(`vue ${this._name} compile`, 'compile', 'compile end')
      }
    }
  }
  return mount.call(this, el, hydrating)
}

理解了上面的流程图, 我们看下今天的核心部分, 也就是 如何 将 templateel 编译成 render 函数的

const { render, staticRenderFns } = compileToFunctions(template, {
outputSourceRange: process.env.NODE_ENV !== 'production',
shouldDecodeNewlines,
shouldDecodeNewlinesForHref, //
delimiters: options.delimiters, // 分隔符
comments: options.comments // 是否保留注释
}, this)
options.render = render
options.staticRenderFns = staticRenderFns

compileToFunctions顾名思义, 就是将模板编译成函数, 那么 它是如何工作的呢?

  1. 在这里我们看到 compileToFunctions 是由 createCompiler(baseOptions) 生成的

  2. createCompiler 是由 createCompilerCreator(baseCompile) 生成的

  3. createCompilerCreator返回了 compileToFunctions函数

  4. 但是 发现 compileToFunctions函数又是由 createCompileToFunctionFn(compile) 生成的 我们整理一下 流程

graph TB
createCompilerCreator --baseCompile函数--> createCompiler -- baseOptions --> compile --> compileToFunctions
createCompileToFunctionFn --> compileToFunctions

流程看清楚了, 我们现在开始阅读源码;

createCompilerCreator

因为 createCompiler 在调用 createCompilerCreator的时候传入了一个 baseCompile 函数,我们现在只需要知道 baseCompile 函数 是用来编译我们的模板的就行了, 这个函数的源码解析,我们会在下一篇文章发布;

createCompilerCreator的基本结构:

image.png 看到这个基本结构, 其实就可以看到 闭包的影子了, 当我们在 调用 createCompilerCreator函数的时候, 传入了 baseCompile 函数, 这样我们 在后序的对模版编译的时候, 不需要 再传入 baseCompile函数了; 然后返回 createCompiler 函数

createCompiler

在调用 createCompiler函数的时候, 传入了一个 baseOptions 参数, 这是用来辅助 baseCompile函数, 生成我们想要的编译函数

graph LR
baseOptions --> compile
baseCompile --> compile

createCompileToFunctionFn

在我们通过 调用 createCompiler的时候, compileToFunctions 是由 createCompileToFunctionFn 结合 我们自己的定制的编译函数, 这样,我们下次编译模板的时候, 只需要 调用 compileToFunctions函数, 不需要重新定制编译函数了

compileToFunctions

前面的一系列操作, 都是为 生成 compileToFunctions 作准备的, 为了 阅读清晰,我删除了一些 开发的输出

export function createCompileToFunctionFn (compile: Function): Function {
  const cache = Object.create(null) // 缓存策略

  // 返回 compileToFunctions 函数
  // 这就是我们文章一开始 编译模板的函数
  return function compileToFunctions (
    template: string, // 我们传入模板
    options?: CompilerOptions, // 传入的 编译选项
    vm?: Component
  ): CompiledFunctionResult {
    options = extend({}, options)
    const warn = options.warn || baseWarn
    delete options.warn

    // check cache 设置 key, 用于 读取 或者 缓存 已经编译的模板
    const key = options.delimiters 
      ? String(options.delimiters) + template
      : template
    if (cache[key]) { // 如果存在 那么直接返回
      return cache[key]
    }

    // 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)
  }
}

因为我们 通过 compile 生成的后的 render 是一个字符串,所以我们要转成 函数; 关于 with 的用法,读者可以查阅其他资料, 这里不做赘述; image.png

综上, 我们可以基本上了解到 Vue模板的基本的步骤

  1. 首先 根据 baseCompile 通过 createCompilerCreator 生成 创建编译器createCompiler 函数
  2. baseOptions 通过 创建编译器 createCompiler 函数 生成我们想要的 模板编译函数 compile
  3. compile 通过 createCompileToFunctionFn生成 我们 需要的 compileToFunctions 函数
  4. 当我们调用 compileToFunctions 函数时, 会根据我们的传入的 模板和配置 ,生成 渲染函数

自此, 模板编译的整体流程,已经结束, 后面我们会解析,