模板的本质

155 阅读4分钟

模板,相信大家在使用框架进行开发的时候并不陌生,使用模块进行开发能够极大的提高开发的效率,这次我们就模板的本质进行一下探讨

渲染函数

渲染函数大家都不陌生,在vue中使用了h函数进行创建虚拟dom,但是实际上,Vue 里面的单文件组件是会被一个 模板编译器 进行编译的,编译后的结果并不存在什么模板,而是会把模板编译为渲染函数的形式。 大家肯定都知道可以使用模板来进行书写组件,写在 <template 标签中,但是,我们还要明白,只使用js,我们也是可以完成组件的开发的,文件的内部直接调用渲染函数来描述你的组件视图。

下面看个例子

<template>
  <div class="user-card">
    <img :src="user.avatarUrl" alt="User avatar" class="avatar" />
    <div class="user-info">
      <h2>{{ user.name }}</h2>
      <p>{{ user.email }}</p>
    </div>
  </div>
</template>

下面是纯使用js的形式来写

import { defineComponent, h } from 'vue'
import styles from './UserCard.module.css'
export default defineComponent({
  name: 'UserCard',
  props: {
    name: String,
    email: String,
    avatarUrl: String
  },
  setup(props) {
    // 下面我们使用了渲染函数的形式来描述了原本在模板中所描述的视图结构
    return () =>
      h(
        'div',
        {
          class: styles.userCard
        },
        [
          h('img', {
            class: styles.avatar,
            src: props.avatarUrl,
            alt: 'User avatar'
          }),
          h(
            'div',
            {
              class: styles.userInfo
            },
            [h('h2', props.name), h('p', props.email)]
          )
        ]
      )
  }
})

这就是完全使用js来进行组件的编写和开发,很显然,原来的文档结构已经被改写成了使用h函数来渲染的深层次的结构,实际上,vue在编译的时候会将你写的模板编译成上述的那种形式,从而实现组件的开发,使用模板仅仅是为了开发者更好的进行组件的开发,实际上在背后仍然是使用了上述的方式

至此我们就知道了,Vue 里面之所以提供模板的方式,是为了让开发者在描述视图的时候,更加的轻松。Vue 在运行的时候本身是不需要什么模板的,它只需要渲染函数,调用这些渲染函数后所得到的虚拟 DOM.

作为一个框架的设计者,你必须要思考:你是框架少做一些,让用户的心智负担更重一些,还是说你的框架多做一些,让用户的心智负担更少一些。

模板编译

那么从我们使用模板开始到最后转变成纯js的代码时,中间又经历了怎么样的变化呢?

单文件组件中所书写的模板,对于模板编译器来讲,就是普通的字符串。

模板内容:

<template>
    <div>
    <h1 :id="someId">Hello</h1>
  </div>
</template>

对于模板编译器来讲,仅仅是一串字符串:


'<template><div><h1 :id="someId">Hello</h1></div></template>'

模板编译器需要对上面的字符串进行操作,最终生成的结果:


function render(){
  return h('div', [
    h('h1', {id: someId}, 'Hello')
  ])
}

模板编译器在对模板字符串进行编译的时候,是一点一点转换而来的,整个过程:

  • 解析器:负责将模板字符串解析为对应的模板AST
  • 转换器:负责将模板AST转换为 JS AST
  • 生成器:将 JS AST 生成最终的渲染函数

每一个部件都依赖于上一个部件的执行结果。 过程大致如下

首先对于模板编译器来讲,会将我们写的模板转换为字符串的形式

"<div><p>Vue</p><p>React</p></div>"

然后解析器拿到这段字符串进行解析,得到一个个的对象

[
  {"type": "tag","name": "div"},
  {"type": "tag","name": "p"},
  {"type": "text","content": "Vue"},
  {"type": "tagEnd","name": "p"},
  {"type": "tag","name": "p"},
  {"type": "text","content": "React"},
  {"type": "tagEnd","name": "p"},
  {"type": "tagEnd","name": "div"}
]

接下来根据解析的内容生成抽象语法树,即(模板的AST) 大致如下

{
  "type": "Root",
  "children": [
    {
      "type": "Element",
      "tag": "div",
      "children": [
        {
          "type": "Element",
          "tag": "p",
          "children": [
              {
                "type": "Text",
                "content": "Vue"
              }
          ]
        },
        {
          "type": "Element",
          "tag": "p",
          "children": [
              {
                "type": "Text",
                "content": "React"
              }
          ]
        }
      ]
    }
  ]
}

接下来转换器将上一步模板AST转换为js ASt(感兴趣可以自己了解一下) 然后将js ASt转换为具体的js代码

function render () {
	return h('div', [h('p', 'Vue'), h('p', 'React')])
}

整体结构大致如下

function compile(template){
  // 1. 解析器
  const ast = parse(template)
  // 2. 转换器:将模板 AST 转换为 JS AST
  transform(ast)
  // 3. 生成器
  const code = genrate(ast)
  return code;
}

至此,我们所写的模板字符就会在编译的时候转换为js的形式进行生成

编译时机

编译的时机大概分为两种情况

  1. 运行时编译
  2. 预编译

运行时编译

所谓运行时编译就是我们平常在本地启动项目的时候,会进行运行时编译

预编译

预编译是发生在工程化环境下面。

所谓预编译,指的是工程打包过程中就完成了模板的编译工作,浏览器拿到的是打包后的代码,是完全没有模板的。