Part3-2-3 Vue.js 源码剖析-模板编译

Posted 沿着路走到底

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Part3-2-3 Vue.js 源码剖析-模板编译相关的知识,希望对你有一定的参考价值。

模板编译

模板编译的主要目的是将模板 (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')
  ])
}

模板编译的作用

Vue 2.x 使用 VNode 描述视图以及各种交互,用户自己编写 VNode 比较复杂

用户只需要编写类似 html 的代码 - Vue 模板,通过编译器将模板转换为返回 VNode 的 render 函数
.vue 文件会被 webpack 在构建的过程中转换成 render 函数

带编译器版本的 Vue.js 中,使用 template 或 el 的方式设置模板

<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')
        }
      } 
    })
  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))]), // _s 是 toString()
        _v(" "),
        _c("comp", { on: { myclick: handler } }),
      ],
      1
    ); 
  }
});

_c 是 createElement() 方法,定义的位置 instance/render.js 中

相关的渲染函数(_开头的方法定义),在 instance/render-helps/index.js 中

把 template 转换成 render 的入口 src\\platforms\\web\\entry-runtime-with-compiler.js

// instance/render-helps/index.js
target._v = createTextVNode
target._m = renderStatic
// core/vdom/vnode.js
export function createTextVNode (val: string | number) {
  return new VNode(undefined, undefined, undefined, String(val))
}
// 在 instance/render-helps/render-static.js export function renderStatic (
  index: number,
  isInFor: boolean
): VNode | Array<VNode> {
  const cached = this._staticTrees || (this._staticTrees = [])
  let tree = cached[index]
  // if has already-rendered static tree and not inside v-for,
  // we can reuse the same tree.
  if (tree && !isInFor) {
    return tree
}
  // otherwise, render a fresh tree.
  tree = cached[index] = this.$options.staticRenderFns[index].call(
    this._renderProxy,
    null,
    this // for render fns generated for functional component templates
  )
  markStatic(tree, `__static__${index}`, false)
  return tree
}

把 template 转换成 render 的入口 src\\platforms\\web\\entry-runtime-with-compiler.js

Vue Template Explorer

vue-template-explorer

https://template-explorer.vuejs.org/#%3Cdiv%20id%3D%22app%22%3E%0A%20%20%3Cselect%3E%0A%20%20%20%20%3Coption%3E%0A%20%20%20%20%20%20%7B%7B%20msg%20%20%7D%7D%0A%20%20%20%20%3C%2Foption%3E%0A%20%20%3C%2Fselect%3E%0A%20%20%3Cdiv%3E%0A%20%20%20%20hello%0A%20%20%3C%2Fdiv%3E%0A%3C%2Fdiv%3E

Vue 2.6 把模板编译成 render 函数的工具

vue-next-template-explorer

https://vue-next-template-explorer.netlify.app/#%7B%22src%22%3A%22%3Cdiv%20id%3D%5C%22app%5C%22%3E%5Cn%20%20%3Cselect%3E%5Cn%20%20%20%20%3Coption%3E%5Cn%20%20%20%20%20%20%7B%7B%20msg%20%20%7D%7D%5Cn%20%20%20%20%3C%2Foption%3E%5Cn%20%20%3C%2Fselect%3E%5Cn%20%20%3Cdiv%3E%5Cn%20%20%20%20hello%5Cn%20%20%3C%2Fdiv%3E%5Cn%3C%2Fdiv%3E%22%2C%22options%22%3A%7B%22mode%22%3A%22module%22%2C%22filename%22%3A%22Foo.vue%22%2C%22prefixIdentifiers%22%3Afalse%2C%22hoistStatic%22%3Afalse%2C%22cacheHandlers%22%3Afalse%2C%22scopeId%22%3Anull%2C%22inline%22%3Afalse%2C%22ssrCssVars%22%3A%22%7B%20color%20%7D%22%2C%22compatConfig%22%3A%7B%22MODE%22%3A3%7D%2C%22whitespace%22%3A%22condense%22%2C%22bindingMetadata%22%3A%7B%22TestComponent%22%3A%22setup-const%22%2C%22setupRef%22%3A%22setup-ref%22%2C%22setupConst%22%3A%22setup-const%22%2C%22setupLet%22%3A%22setup-let%22%2C%22setupMaybeRef%22%3A%22setup-maybe-ref%22%2C%22setupProp%22%3A%22props%22%2C%22vMySetupDir%22%3A%22setup-const%22%7D%2C%22optimizeBindings%22%3Afalse%7D%7D

Vue 3.0 beta 把模板编译成 render 函数的工具

模板编译过程

解析、优化、生成

编译的入口

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

Vue.prototype.$mount = function (
  ......
// 把 template 转换成 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() 执行过程,生成渲染函数的过程

compileToFunctions:   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)
    const warn = options.warn || baseWarn
    delete options.warn

    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production') {
      // detect possible CSP restriction
      try {
        new Function('return 1')
      } catch (e) {
        if (e.toString().match(/unsafe-eval|CSP/)) {
          warn(
            'It seems you are using the standalone build of Vue.js in an ' +
            'environment with Content Security Policy that prohibits unsafe-eval. ' +
            'The template compiler cannot work in this environment. Consider ' +
            'relaxing the policy to allow unsafe-eval or pre-compiling your ' +
            'templates into render functions.'
          )
        }
      }
    }

    // check cache
    // 1. 读取缓存中的 CompiledFunctionResult 对象,如果有直接返回
    const key = options.delimiters
      ? String(options.delimiters) + template
      : template
    if (cache[key]) {
      return cache[key]
    }

    // compile
    // 2. 把模板编译为编译对象(render, staticRenderFns),字符串形式的js代码
    const compiled = compile(template, options)

    // check compilation errors/tips
    if (process.env.NODE_ENV !== 'production') {
      if (compiled.errors && compiled.errors.length) {
        if (options.outputSourceRange) {
          compiled.errors.forEach(e => {
            warn(
              `Error compiling template:\\n\\n${e.msg}\\n\\n` +
              generateCodeFrame(template, e.start, e.end),
              vm
            )
          })
        } else {
          warn(
            `Error compiling template:\\n\\n${template}\\n\\n` +
            compiled.errors.map(e => `- ${e}`).join('\\n') + '\\n',
            vm
          )
        }
      }
      if (compiled.tips && compiled.tips.length) {
        if (options.outputSourceRange) {
          compiled.tips.forEach(e => tip(e.msg, vm))
        } else {
          compiled.tips.forEach(msg => tip(msg, vm))
        }
      }
    }

    // turn code into functions
    const res = {}
    const fnGenErrors = []

    // 3. 把字符串形式的js代码转换成js方法
    res.render = createFunction(compiled.render, fnGenErrors)
    res.staticRenderFns = compiled.staticRenderFns.map(code => {
      return createFunction(code, fnGenErrors)
    })

    // check function generation errors.
    // this should only happen if there is a bug in the compiler itself.
    // mostly for codegen development use
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production') {
      if ((!compiled.errors || !compiled.errors.length) && fnGenErrors.length) {
        warn(
          `Failed to generate render function:\\n\\n` +
          fnGenErrors.map(({ err, code }) => `${err.toString()} in\\n\\n${code}\\n`).join('\\n'),
          vm
        )
      }
    }
    // 4. 缓存并返回res对象(render, staticRenderFns方法)
    return (cache[key] = res)
  }
}

complie(template, options):  src\\compiler\\create-compiler.js

export function createCompilerCreator (baseCompile: Function): Function {
  // baseOptions 平台相关的options
  // src\\platforms\\web\\compiler\\options.js 中定义
  return function createCompiler (baseOptions: CompilerOptions) {
    function compile (
      template: string,
      options?: CompilerOptions
    ): CompiledResult {
      const finalOptions = Object.create(baseOptions)
      const errors = []
      const tips = []

      let warn = (msg, range, tip) => {
        (tip ? tips : errors).push(msg)
      }

      if (options) {
        if (process.env.NODE_ENV !== 'production' && options.outputSourceRange) {
          // $flow-disable-line
          const leadingSpaceLength = template.match(/^\\s*/)[0].length

          warn = (msg, range, tip) => {
            const data: WarningMessage = { msg }
            if (range) {
              if (range.start != null) {
                data.start = range.start + leadingSpaceLength
              }
              if (range.end != null) {
                data.end = range.end + leadingSpaceLength
              }
            }
            (tip ? tips : errors).push(data)
          }
        }
        // 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]
          }
        }
      }

      finalOptions.warn = warn

      const compiled = baseCompile(template.trim(), finalOptions)
      if (process.env.NODE_ENV !== 'production') {
        detectErrors(compiled.ast, warn)
      }
      compiled.errors = errors
      compiled.tips = tips
      return compiled
    }

    return {
      compile,
      compileToFunctions: createCompileToFunctionFn(compile)
    }
  }
}

baseCompile(template.trim(), finalOptions):    src\\compiler\\index.js

/* @flow */

import { parse } from './parser/index'
import { optimize } from './optimizer'
import { generate } from './codegen/index'
import { createCompilerCreator } from './create-compiler'

// `createCompilerCreator` allows creating compilers that use alternative
// parser/optimizer/codegen, e.g the SSR optimizing compiler.
// Here we just export a default compiler using the default parts.
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: code.render,
    // 静态渲染函数,生成静态 VNode 树
    staticRenderFns: code.staticRenderFns
  }
})

什么是抽象语法树

抽象语法树简称 AST(Abstract Syntax Tree)

使用对象的形式描述树形的代码结构

此处的抽象语法树是用来描述树形结构的 HTML 字符串

为什么要使用抽象语法树

模板字符串转换成 AST 后,可以通过 AST 对模板做优化处理

标记模板中的静态内容,在 patch 的时候直接跳过静态内容

在 patch 的过程中,静态内容不需要对比和重新渲染

解析 - parse

解析器将模板解析为抽象语树 AST,只有将模板解析成 AST 后,才能基于它做优化或者生成代码

字符串。

src\\compiler\\index.js

const ast = parse(template.trim(), options)
//src\\compiler\\parser\\index.js
parse()

查看得到的 AST tree

https://astexplorer.net/#/gist/30f2bd28c9bbe0d37c2408e87cabdfcc/1cd0d49beed22d3fc8e2ade0177bb22bbe4b907c

结构化指令的处理

v-if 最终生成单元表达式

// src\\compiler\\parser\\index.js // structural directives
// 结构化的指令
// v-for
processFor(element)
processIf(element)
processOnce(element)
// src\\compiler\\codegen\\index.js
export function genIf (
  el: any,
  state: CodegenState,
  altGen?: Function,
  altEmpty?: string
): string {
  el.ifProcessed = true // avoid recursion
  return genIfConditions(el.ifConditions.slice(), state, altGen, altEmpty)
}
// 最终调用 genIfConditions 生成三元表达式

v-if 最终编译的结果

ƒ anonymous( ){
  with(this){
    return _c('div',{attrs:{"id":"app"}},[
      _m(0),
      _v(" "),
      (msg)?_c('p',[_v(_s(msg))]):_e(),_v(" "),
      _c('comp',{on:{"myclick":onMyClick}})
],1) }
}

v-if/v-for 结构化指令只能在编译阶段处理,如果我们要在 render 函数处理条件或循环只能使用 js 中的 if 和 for

Vue.component('comp', {
  data: () {
    return {
      msg: 'my comp'
} },
  render (h) {
    if (this.msg) {
      return h('div', this.msg)
    }
    return h('div', 'bar')
  }
})

优化 - optimize

优化抽象语法树,检测子节点中是否是纯静态节点

一旦检测到纯静态节点,例如:

hello整体是静态节点

永远不会更改的节点

提升为常量,重新渲染的时候不在重新创建节点

在 patch 的时候直接跳过静态子树

// src\\compiler\\index.js
if (options.optimize !== false) {
    // 优化抽象语法树
    optimize(ast, options)
}

// src\\compiler\\optimizer.js
/**
 * Goal of the optimizer: walk the generated template AST tree
 * and detect sub-trees that are purely static, i.e. parts of
 * the DOM that never needs to change.
 *
 * Once we detect these sub-trees, we can:
 *
 * 1. Hoist them into constants, so that we no longer need to
 *    create fresh nodes for them on each re-render;
 * 2. Completely skip them in the patching process.
 */
export function optimize (root: ?ASTElement, options: CompilerOptions) {
  if (!root) return
  isStaticKey = genStaticKeysCached(options.staticKeys || '')
  isPlatformReservedTag = options.isReservedTag || no
  // first pass: mark all non-static nodes.
  // 标记静态节点
  markStatic(root)
  // second pass: mark static roots.
  // 标记静态根节点
  markStaticRoots(root, false)
}

生成 - generate

// src\\compiler\\index.js
const code = generate(ast, options)
// 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
  }
}
// 把字符串转换成函数
// src\\compiler\\to-function.js
function createFunction (code, errors) {
  try {
    return new Function(code)
  } catch (err) {
    errors.push({ err, code })
    return noop
} }

1

以上是关于Part3-2-3 Vue.js 源码剖析-模板编译的主要内容,如果未能解决你的问题,请参考以下文章

剖析 Vue.js 内部运行机制

福利剖析 Vue.js 内部运行机制小册

Part3-2-2 Vue.js 源码剖析-虚拟 DOM

是否可以将 Vue.js 模板编译为静态 HTML 和 CSS 文件?

Part3-2-1 Vue.js 源码剖析-响应式原理

初步了解VUE源码