Vue 100 问(34-35问)讲一下 Vue 模板编译的过程

1,137 阅读4分钟

「这是我参与2022首次更文挑战的第15天,活动详情查看:2022首次更文挑战」。

34.说一下 Vue 模版和 render 函数

Vue模版(template)是什么?

Vue模版(template) 是 Vue 提供的类html语法,用来书写页面结构,可以在上面写html、指令、插值和 js 表达式,语法易于理解,书写简单,但缺点是不够灵活。

render 函数是什么?

渲染函数(render 函数)是 Vue 提供的另一种书写页面的方式,Vue 除了可以用 template 写页面,也可以用 render 函数结合 createElement 方法或者 JSX 书写页面,render 函数比 template 更接近编译器。

createElement 是什么?

createElement 是 Vue 提供的一个函数,可以通过传入 tag(元素名)、data(属性)和 children(子元素) 这三个参数,来生成一个 VNode

createElement 在 Vue 源码中定义如下:

$createElement: (tag?: string | Component, data?: Object, children?: VNodeChildren) => VNode;

JSX是什么?

JSX 是一个 JavaScript 的语法扩展,可以很好地描述 UI 应该呈现出它应有交互的本质形式,并且具有 JavaScript 的全部功能,十分灵活。 -- react 文档

在 Vue 开发中,使用 render 函数 + JSX 来开发,比使用 render 函数 + createElement 写起来方便很多。

我们用一张图来总结一下常用组件写法:

image.png

日常开发大多数情况都是使用 template 开发组件,但某些特殊情况使用 render 函数 + JSX 来开发更方便。

三种组件写法的区别

举个例子,假设我们要实现一个标题组件,根据传入的 level 值决定用 h1 - h6 中的某一个标签。

我们用上面说的三种方式分别实现一下这个组件:

template

<custom-title :level="1">hello world</custom-title>
CustomTitle 组件内部

<div>
  <h1 v-if="level === 1">
    <slot></slot>
  </h1>
  <h2 v-if="level === 2">
    <slot></slot>
  </h2>
  <h3 v-if="level === 3">
    <slot></slot>
  </h3>
  <h4 v-if="level === 4">
    <slot></slot>
  </h4>
  <h5 v-if="level === 5">
    <slot></slot>
  </h5>
  <h6 v-if="level === 6">
    <slot></slot>
  </h6>
</div>

props: {
  level: {
    type: Number,
    default: 1
  }
}

使用 template,代码比较冗长,且写了很多重复的逻辑。

render函数 + createElement

props: {
  level: {
    type: Number,
    default: 1
  }
},
render(createElement) {
  return createElement('h' + this.level, this.$slots.default)
}

使用 render 函数 + createElement,就简洁多了,但是代码不容易被理解,需要理解 createElement 方法。

render函数 + JSX

props: {
  level: {
    type: Number,
    default: 1
  }
},
render() {
  const Tag = `h${this.level}`
  return <Tag>{this.$slots.default}</Tag>
}

使用 render 函数 + JSX,代码简洁,且代码容易理解,虽然也全是 js 语法,但看着像 html。

再举一个例子,这样一段代码:

<custom-title :level="1"> 
  <span>Hello</span> world! 
</custom-title>

使用 createElement 编写:

render: function (createElement) {
  return createElement(
    'custom-title', {
      props: {
        level: 1
      }
    }, [
      createElement('span', 'Hello'),
      ' world!'
    ]
  )
}

使用 JSX 编写:

render: function () {
  return (
    <CustomTitle level={1}>
      <span>Hello</span> world!
    </CustomTitle>
  )
}

很明显,使用 JSX 的体验好很多。

注意:vue-cli 3.0版本以上自动支持 JSX,如果是自己搭的 Vue 项目,就需要引入一些 babel 插件了,比如 @vue/babel-preset-jsx

35.讲一下 Vue 模板编译的过程

在 Vue 文档中,有一个实时编译模板字符串的示例,地址在这里

这样一个模板:

<div>
  <p>{{ message }}</p>
</div>

会被编译为:

function anonymous(
) {
  with(this){return _c('div',[_c('p',[_v(_s(message))])])}
}

这是如何实现的呢,我们来分析一下。

with语句

我们看到,被编译出的 js 代码使用了 with 语句。

with 这个语法不常用也不推荐用,不过在 Vue 模板编译的过程中用到了,我们就来学习一下这个语法。

with 语句的用途是将代码作用域设置为特定的对象,其语法是:

with (expression) {
    statement
}
  • 它可以改变代码块内自由变量的查找规则,当做对象的属性来查找。
  • 如果找不到匹配的对象属性,会报错。 举个例子:

image.png

name 和 age 都打印出来了,打印 sex 报错。

with 要慎用,它打破了作用域规则,易读性变差。请看下面的例子:

function f(x, obj) {
  with (obj)
    print(x);
}

x的值就不知道取函数的值,还是对象 obj 的值。

跑题了,更多请查看 mdn

我们继续来分析 with 在 Vue 源码中的使用。

with(this){return _c('div',[_c('p',[_v(_s(message))])])}

这里的 with 就是为了让代码更简洁,代码块里的 _c_v_smessage,其实就是

this._c
this._v
this._s
this.message

我们知道,this 是 vm 实例,this.message 好理解,但是 this._c、this._v、this._s 是啥?

函数缩写

看了 Vue 源码才知道,这三个东西其实是函数名的缩写

this._c = function (a, b, c, d) { return createElement(contextVm, a, b, c, d, needNormalization); };
export function installRenderHelpers (target: any) {
  ...
  target._s = toString
  ...
  target._v = createTextVNode
  ...
}
_c: (
  vnode?: VNode,
  data?: VNodeData,
  children?: VNodeChildren,
  normalizationType?: number
) => VNode | void;

_s: (value: mixed) => string;

_v: (value: string | number) => VNode;

_c 是·createElement,_s 是 toString, _v 是 createTextVNode

翻译一下:

<div> 
  <p>{{ message }}</p> 
</div>

with(this){return _c('div',[_c('p',[_v(_s(message))])])}

with(this){
  return createElement('div', [
    createElement('p', [
      createTextVNode(toString(message))
    ])
  ])
}

是不是感觉能看懂了,类 html 的 template 被转成了 js 的代码。

编译过程

那么 template 被转成 js 的过程是怎样的呢?可以总结成下面的几步

  • 解析模板字符串生成 AST(抽象语法树)
  • 优化 AST,方便后续虚拟 DOM 更新
  • 将 AST 转化为可执行的代码(render函数)
  • 执行 render 函数生成 VNode

在 Vue 源码中是这样的:

const baseCompile = (template, options) => {
  const ast = parse(template.trim(), options) // 解析模板字符串生成 AST
  optimize(ast, options)                      // 优化 AST,方便后续虚拟 DOM 更新
  const code = generate(ast, options)         // 将 AST 转化为可执行的代码
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
}

中间过程很复杂,阿林确实肝不动,更多详细的过程可以参考这两篇文章:

小结

Vue 模板编译的过程就是 tempalte -> render 函数 -> VNode,最终生成一个虚拟DOM,为响应式做准备。

如果我的文章对你有帮助,你的赞👍就是对我的最大支持^ _ ^

往期

从 keep-alive 源码掌握 LRU Cache

Vue 100 问 (30-33问)Vue 组件之间的通信方式有哪些?

Vue 100问(27-29)说一下你对 vue 插槽的理解

Vue 100问(23-26问)vue 指令的本质是什么?

Vue 100问(18-22问)谈谈你对 Vue 生命周期的理解

Vue100问(第6-17问)

Vue100问(第5问)为什么 v-for 的 key 值不能是 index?

Vue100问(第4问)说一下你平时用过的 Vue 修饰符

Vue100问(2-3问)什么场景下会使用.sync修饰符?

Vue100问(一)不使用v-model,如何实现双向绑定?