Vue.js 框架源码与进阶 - 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.js 模板,通过编译器将模板转换为返回 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对象的成员可省略this
  with (this) {
    return _c(
      "div", // tag标签,对应<div>
      { attrs: { id: "app" } }, // data描述tag,对应id="app"
      [ // children设置tag子节点
        _m(0), // 处理静态内容做优化处理,对应<h1>
        _v(" "), // 创建空白的文本节点,对应<h1>和<p>之间的空白位置(换行)
        // 创建<p>对应的vnode 第二个位置(数组包裹的文本的vnode节点)
        _c("p", [_v(_s(msg))]), // 把用户输入数据转化为字符串(_s)
        _v(" "),
        _c("comp", { on: { myclick: handler } }), // 创建自定义组件对应的vnode
      ],
      1 // 后续如何对children处理,将children拍平为一维数组
    );
  }
});
  • _c 是 createElement() 方法,定义的位置 instance/render.js 中
  • 相关的渲染函数(_开头的方法定义),在 instance/render-helps/index.js 中
// instance/render-helps/index.js
target._v = createTextVNode
target._s = toString
target._m = renderStatic

// core/vdom/vnode.js
export function createTextVNode (val: string | number) {
  return new VNode(undefined, undefined, undefined, String(val))
}

// shared/util
// 将一个值转换为实际渲染的字符串
export function toString (val: any): string {
  return val == null
    ? ''
    : Array.isArray(val) || (isPlainObject(val) && val.toString === _toString)
      ? JSON.stringify(val, null, 2)
      : 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.
  // 如果已经渲染了静态树,并且不在v-for里面,我们可以重用同样的树。
  if (tree && !isInFor) {
    return tree
  }
  // otherwise, render a fresh tree.
  // 如果没有,从staticRenderFns这个数组中获取静态根节点对应的render函数调用
  // 此时就生成vnode节点,把结果缓存
  tree = cached[index] = this.$options.staticRenderFns[index].call(
    this._renderProxy,
    null,
    this // for render fns generated for functional component templates
  )
  // 把当前返回的vnode节点标记为静态的
  // 将来调用patch函数的时候,内部会判断如果当前vnode为静态,则不再对比节点差异
  markStatic(tree, `__static__${index}`, false)
  return tree
}
  • 把 template 转换成 render 的入口 src\platforms\web\entry-runtime-with-compiler.js

三、Vue Template Explorer

把 html 模版转换成 render 函数的工具

  • vue-template-explorer
    • Vue 2.6 把模板编译成 render 函数的工具
    • 在使用vue2.x 的模板时,标签内的文本内容尽量不要添加多余的空白

模板

<div id="app">
  <select>
    <option>
      {{ msg  }}
    </option>
  </select>
  <div>
    hello
  </div>
</div>

转换结果

function render() {
  with(this) {
    return _c('div', {
      attrs: {
        "id": "app"
      }
    }, [_c('select', [_c('option', [_v("\n      " + _s(msg) + "\n    ")])]),
      _c('div', [_v("\n    hello\n  ")])
    ])
  }
}
  • vue-next-template-explorer
    • Vue 3.0 beta 把模板编译成 render 函数的工具
    • vue3 编译后的 render 函数已经去除了标签内多余的空白
import { toDisplayString as _toDisplayString, createElementVNode as _createElementVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", { id: "app" }, [
    _createElementVNode("select", null, [
      _createElementVNode("option", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)
    ]),
    _createElementVNode("div", null, " hello ")
  ]))
}

// Check the console for the AST

四、编译的入口函数

  • 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
  ...
)

在这里插入图片描述

五、模板编译过程

5.1 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 {
    // 防止污染 vue 的 options 所以克隆一份
    options = extend({}, options)
    const warn = options.warn || baseWarn
    delete options.warn
    ...

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

    // 3. 把字符串形式的js代码转换成js方法
    res.render = createFunction(compiled.render, fnGenErrors)
    res.staticRenderFns = compiled.staticRenderFns.map(code => {
      return createFunction(code, fnGenErrors)
    })
    ...
    
    // 4. 缓存并返回res对象(render, staticRenderFns方法)
    return (cache[key] = res)
  }
}

5.2 compile

  • 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 {
      // 合并 baseOptions 和 complice函数传递过来的options
      const finalOptions = Object.create(baseOptions)
      // 存贮编译过程中出现的错误和信息
      const errors = []
      const tips = []

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

      if (options) {
        ...
      }

      finalOptions.warn = warn

      // 通过 baseCompile 把模板编译成 render函数
      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)
    }
  }
}

5.3 baseCompile

  • src/compiler/index.js
// `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.
// `createCompilerCreator`允许创建使用替代解析器/优化器/代码生成的编译器,
// 例如SSR优化编译器。在这里,我们只是使用默认的部分导出一个默认的编译器。
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
  }
})

5.3.1 baseCompile-AST

什么是抽象语法树

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

为什么要使用抽象语法树

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

获取 AST

5.3.2 baseCompile-parse

  • 解析器将模板解析为抽象语树 AST,只有将模板解析成 AST 后,才能基于它做优化或者生成代码字符串
  • parse 函数内部处理过程中会依次去遍历 html 模板字符串,把其转换成 AST 对象,html 中的属性和指令(v-if、v-for 等)都会记录在 AST 对象的相应属性上
    • src/compiler/index.js
// 把模板转换成 AST 抽象语法树
// 抽象语法树,用来以树形的方式描述代码结构
const ast = parse(template.trim(), options)
  • 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");
        },
      });

5.3.3 baseCompile-optimize

  • src/compiler/index.js
if (options.optimize !== false) {
  // 优化抽象语法树
  optimize(ast, options)
}
  • src/compiler/optimizer.js
  • 优化抽象语法树,检测子节点中是否是纯静态节点(对应的 DOM 子树永远不会发生变化)
  • 一旦检测到纯静态节点
    • 提升为常量,重新渲染的时候不在重新创建节点
    • 在 patch 的时候直接跳过静态子树
/**
 - 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.
 */
// 优化的目的:标记抽象语法树的静态节点,即DOM中永远不需要改变的部分
// 当标记完静态子树后,将来就不需要进行渲染,在patch的时候直接跳过静态子树
// 一旦我们检测到这些子树,我们就可以做到: 
// 1. 将它们提升为常量,这样我们就不再需要在每次重新渲染时为它们创建新的节点;
// 2. 在修补过程中完全跳过它们。
export function optimize (root: ?ASTElement, options: CompilerOptions) {
  // 判断root,是否传递 AST 对象
  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)
}

5.3.4 baseCompile-generate

  • src/compiler/index.js
// 把抽象语法树生成字符串形式的 js 代码
const code = generate(ast, options)
  • src/compiler/codegen/index.js
  • 把抽象语法树转换成字符串形式的 js 代码,生成 render 表达式
export function generate (
  ast: ASTElement | void,
  options: CompilerOptions
): CodegenResult {
  // 代码生成过程中使用到的状态对象
  const state = new CodegenState(options)
  // AST存在,调用genElement生成代码
  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
  }
}

5.4 模板编译过程总结

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

每天内卷一点点

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值