vue模版编译

模板编译的作用

​ 主要目的是将template转换为渲染函数(render)

<div>
  <h1 @click="handler">title</h1>
  <p>some content</p>
</div>

​ 渲染函数 render

render(h) {
  return h('div', [
    h('h1', { on: { click: this.handler } }, 'title'),
    h('p', 'some content')
  ])
}

​ Vue2.x使用VNode来描述视图及交互,自己手写比较麻烦。通过编译器可以将模板转换为返回 VNode 的 render 函数,要注意的是.vue 文件会被 webpack 在构建的过程中(vue-loader)转换成 render 函数

体验模板编译的结果

<div id="app">
  <h1>Vue<span>模板编译过程</span></h1>
  <p>{{ msg }}</p>
  <comp @myclick="handler"></comp>
</div>
<script src="../../dist/vue.js"></script>
<script>
  Vue.component('comp', {
    template: '<div>I am a comp</div>'
  })
  const vm = new Vue({
    el: '#app',
    data: {
      msg: 'Hello compiler'
    },
    methods: {
      handler() {
        console.log('test')
      }
    }
  })
  // 打印编译生成的render函数
  console.log(vm.$options.render)
</script>

​ 编译后 render 输出的结果

(function anonymous() {
  with (this) {
    return _c(
      "div",
      { attrs: { id: "app" } },
      [
        _m(0),
        _v(" "),
        _c("p", [_v(_s(msg))]),
        _v(" "),
        _c("comp", { on: { myclick: handler } }),
      ],
      1
    );
  }
});
  • _c 是 createElement() 方法,定义的位置 instance/render.js 中
  • 相关的渲染函数(_开头的方法定义),在 instance/render-helps/index.js 中
  • 对比vue2和vue3的模版编译结果,vue2不会过滤掉多余的空格,所以要少写空格以优化代码

编译的入口

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

  • 拓展了$mount方法
  • 解析了el,template等选项,根据模版生成了render
// 可以看到在这里引入了Vue
import Vue from './runtime/index'

// 拓展了$mount方法,说明除了它本来要做的一些事情还要做
// 一些其他的事情,在web平台上
const mount = Vue.prototype.$mount
// 定义了$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
  // 先判断有没有render函数(用户传入的)
  // 说明render优先级最高,其次template
  if (!options.render) {
    let template = options.template
    // 没有传递的化继续判断是否有template
    if (template) {
      if (typeof template === 'string') {
        if (template.charAt(0) === '#') {
          // 如果有就是我们的模版
          template = idToTemplate(template)
        }
      } else if (template.nodeType) {
        template = template.innerHTML
      } else {
        return this
      }
    } else if (el) {
      // 如果没有template,el的OuterHTML作为模版
      // 也就是说我们的模版就是template或者el
      template = getOuterHTML(el)
    }
    // 最终获取模版之后编译它
    if (template) {
      // 可以看到编译最终目标就是获取render,和一个静态renderr方法
      const { render, staticRenderFns } = compileToFunctions(template, {
        outputSourceRange: process.env.NODE_ENV !== 'production',
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      // 还会赋值给options
      options.render = render
      options.staticRenderFns = staticRenderFns
    }
  }
  // 执行默认的挂载功能
  return mount.call(this, el, hydrating)
}

function getOuterHTML (el: Element): string {
  if (el.outerHTML) {
    return el.outerHTML
  } else {
    const container = document.createElement('div')
    container.appendChild(el.cloneNode(true))
    return container.innerHTML
  }
}

Vue.compile = compileToFunctions

export default Vue

compileToFunctions

const { compile, compileToFunctions } = createCompiler(baseOptions)
// baseOptions是跟web平台相关的选项

​ baseOptions

export const baseOptions: CompilerOptions = {
  expectHTML: true, // 期望的是html的内容
  modules, // 模块和指令
  directives,
  isPreTag, // 是否是pre标签和自闭和标签
  isUnaryTag,
  mustUseProp,
  canBeLeftOpenTag,
  isReservedTag, // 是否是html的保留标签
  getTagNamespace,
  staticKeys: genStaticKeys(modules)
}

createCompiler

export const createCompiler = createCompilerCreator(function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
  // 把模版编译成ast抽象语法树
  const ast = parse(template.trim(), options)
  if (options.optimize !== false) {
    // 优化抽象语法树
    optimize(ast, options)
  }
	// 把抽象语法树转换成字符串形式的js代码
  const code = generate(ast, options)
  return {
    ast,
    // 这里的render不是最终的,是字符串形式的,还要通过toFunction转换成函数形式
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
})

createCompilerCreator

export function createCompilerCreator (baseCompile: Function): Function {
  return function createCompiler (baseOptions: CompilerOptions) {
    function compile ( // 定义了compile函数
      template: string,
      options?: CompilerOptions
    ): CompiledResult {}
		...
    return {
      compile,
      // 返回了这个入口函数🍂
      compileToFunctions: createCompileToFunctionFn(compile)
    }
  }
}

createCompileToFunctionFn

​ 就是入口的compileToFunctions

export function createCompileToFunctionFn (compile: Function): Function {
  // 先定义一个cache缓存编译后的结果
  const cache = Object.create(null)

  return function compileToFunctions (// 🍂
    template: string,
    options?: CompilerOptions,
    vm?: Component
  ): CompiledFunctionResult {
    // 克隆了一份options防止污染
    options = extend({}, options)

    // check cache
    // 判断缓存中是否有编译的结果,把模版的内容作为key
    const key = options.delimiters// 改变差值表达式使用的符号,完整版才有
      ? String(options.delimiters) + template
      : template
    if (cache[key]) {
      return cache[key]
    }

    // compile
    // 这个编译后的对象有render和staticRenderFns
    // 此时的render中存储的是字符串形式的js代码
    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 => {
      // 通过new Function(code)
      return createFunction(code, fnGenErrors)
    })
    // 缓存并返回res对象(render和staticRenderFns方法)
    return (cache[key] = res)
  }
}

​ 到这里编译的入口函数就完了,核心就是缓存编译后的结果,有就直接返回,没有就开始编译,并且把编译后的字符串形式的代码转换成函数形式缓存并返回

模版编译过程

compile

​ 定义在createCompilerCreator里的createCompiler,核心作用就是合并选项,调用baseCompile进行编译

function compile (
  template: string,
  options?: CompilerOptions // 用户传入的选项
): CompiledResult {
  // 用来最后合并参数的options和baseOptions
  const finalOptions = Object.create(baseOptions)
  const errors = []
  const tips = []
  // 开始合并
  if (options) {
    // merge custom modules
    if (options.modules) {
      finalOptions.modules =
        (baseOptions.modules || []).concat(options.modules)
    }
    // merge custom directives
    if (options.directives) {
      finalOptions.directives = extend(
        Object.create(baseOptions.directives || null),
        options.directives
      )
    }
    // copy other options
    for (const key in options) {
      if (key !== 'modules' && key !== 'directives') {
        finalOptions[key] = options[key]
      }
    }
  }
  // 调用模版编译的核心函数开始编译
  const compiled = baseCompile(template.trim(), finalOptions)
  return compiled
}

baseCompile

​ 编译的核心函数,生成AST,分为三步:解析,优化和生成

  // 把模版编译成ast抽象语法树(现在模版是字符串)
  const ast = parse(template.trim(), options)
  if (options.optimize !== false) {
    // 优化抽象语法树
    optimize(ast, options)
  }
	// 把抽象语法树转换成字符串形式的js代码
  const code = generate(ast, options)
  return {
    ast,
    // 这里的render不是最终的,是字符串形式的,还要通过toFunction转换成函数形式
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }

parse - 解析

​ parse内部过于复杂,且借鉴了某个开源库,有三个文件:html-parser / text-parser(差值表达式文本) / filter-parser

export function parse(
  template: string,
  options: CompilerOptions
): ASTElement | void {
  // 先解析options中的成员
  // ...
  // 解析过程
  // <div><span></span></div>
  // 这边是入栈的操作,找到一个开始标签就插入栈中,找到结束标签就出栈
  // 如果栈清空就说明是合法的模版
  const stack = []
  const preserveWhitespace = options.preserveWhitespace !== false
  const whitespaceOption = options.whitespace
  let root

  // 核心的解析算法
  parseHTML(template, {
    warn,
    expectHTML: options.expectHTML,
    isUnaryTag: options.isUnaryTag,
    canBeLeftOpenTag: options.canBeLeftOpenTag,
    shouldDecodeNewlines: options.shouldDecodeNewlines,
    shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,
    shouldKeepComment: options.comments,
    outputSourceRange: options.outputSourceRange,
    // 开始标签,创建一个ast对象,同时把这个开始标签tag插入到刚才那个栈中
    start(tag, attrs, unary, start) {
      // 创建一个ast对象
      let element: ASTElement = createASTElement(tag, attrs, currentParent)
      // ...
      if (inVPre) {
        processRawAttrs(element)
      } else if (!element.processed) {
        // structural directives
        // 结构型指令处理,v-if,v-for,v-once
        processFor(element)
        processIf(element)
        processOnce(element)
      }
    },
    // 结束标签
    end(tag, start, end) {
      // 出栈
    },
    // 文本标签
    chars(text: string, start: number, end: number) {
    },
    // 注释内容
    comment(text: string, start, end) {
    }
  })
  // root就是存储好的ast对象🍂
  return root
}

​ parseHTML

// Regular Expressions for parsing tags and attributes
// 很多正则表达式用来匹配html模版中的内容
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/
const startTagOpen = new RegExp(`^<${qnameCapture}`)
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`)
const doctype = /^<!DOCTYPE [^>]+>/i
const comment = /^<!\--
...
	// 遍历html模版字符串
  // 会把处理完的文本截取掉,继续处理剩余的部分
  while (html) {
    let textEnd = html.indexOf('<')
    if (textEnd === 0) {
      ...
    }
    ...
  }

​ 总结:parse函数会依次遍历html模版字符串,转换成AST对象,html的属性和指令都会记录在AST对象的相应属性上

​ 接下来会对AST进行优化,optimise,优化实际就是标记静态语法树,将来可以跳过不用再比对,ast中最终会被标记static和staticRoot

$

抽象语法树

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

优点:

  • 转成AST后,可以通过AST对模版做优化处理
  • 标记模版中的静态内容,在patch的时候直接跳过静态内容(静态内容不需要对比和重新渲染)

​ 通过children和parent记录父子节点可以生成树的形式

posted @ 2021-05-31 20:27  __前端工程师  阅读(371)  评论(0)    收藏  举报