ShawDubie

Vue 系列(一):初探 Vue3

惯例:一般都从 Hello World 讲起
<template>
  <div @click="helloWorld">
    Hello World
  </div>
</template>

<script lang="ts" setup>
const helloWorld = () => {
  alert('Hello World');
};
</script>

上面是一个我们非常熟悉并且十分简单的 Vue 例子。该段代码在 Vue 框架中执行之后,会渲染出 Hello World 文本,点击该文本会出现一个写有 Hello World 的弹窗。本文将要探讨的问题就是:Vue 在背后用了什么魔法?

虚拟DOM

谈虚拟 DOM 之前,我们先看看真实的 DOM:
<div onclick="helloWorld()">
  Hello World
</div>

这段代码的功能与前面的 Vue 代码一样,不同之处在于,她可以直接在浏览器中运行,而 Vue 代码要想实现同样的效果就需要把上面的代码进行转化,转化为可以直接运行的真实 DOM。那么 Vue 要如何进行转化呢?

Vue 首先会将模板(<template>..</template>)编译为 JavaScript 对象,以上面 Vue 代码为例,通过编译器的一顿操作,将会转换为类似如下的代码:

{
  tag: 'div',
  props: { onClick: helloWorld },
  children: 'Hello World'
}

说明

其实上面的 JavaScript 对象只是编译器处理过程中的一个中间代码,在后面的文章当中我们会详细介绍编译器,这里暂时先简单地理解~~

可以看到实际上就是利用 JavaScript 对象来描述了上面的 UI 代码,而这种通过使用 JavaScript 对象来描述 UI 的方式,就是所谓的 虚拟 DOM。

在编写 Vue 代码的时候,我们不仅仅只是在一个模板中写一下 UI 代码,我们肯定会引入各种各样的 组件,如果我们将上面的 Vue 代码封装成一个名为 HelloWorld 的组件,上述代码将变成:

<template>
  <div>
    <hello-world />
  </div>
</template>

<script lang="ts" setup>
import HelloWorld from 'HelloWorld.vue';
</script>

那我们该如何用虚拟DOM描述组件呢?其实,组件就是一组DOM元素的封装,理解这句话之后我们就可以用虚拟DOM这样去描述组件了:

const HelloWorld = {
  tag: 'div',
  props: { onClick: helloWorld },
  children: 'Hello World'
}

const vnode = {
  tag: HelloWorld
}

可以看到,我们将组件封装成了一个 JavaScript 对象,然后在虚拟DOM中引用这个对象即可,当然,组件也可以是一个函数:

const HelloWorld = () => ({
  tag: 'div',
  props: { onClick: helloWorld },
  children: 'Hello World'
})

const vnode = {
  tag: HelloWorld
}
渲染器

前面我们讲了 Vue 会通过编译器将模板编译为虚拟DOM,那模板是怎么渲染成真实DOM的呢,这就需要了解 Vue 的渲染器了:

Vue 是通过渲染器将虚拟DOM渲染为真实DOM的。先不要想的太复杂,要想实现渲染器,实际上就是实现怎么将虚拟DOM转换为真实DOM,以上面的虚拟DOM为例,我们可以定义如下方法:

const mountElement = (vnode, container) => {
  // 创建标签为tag的DOM元素
  const el = document.createElement(vnode.tag);
  // 将属性、事件添加到元素中,这里只实现了事件
  for (const key in vnode.props) {
    // 以 on 开头,说明是事件
    if (/^on/.test(key)) {
      el.addEventListener(key.substr(2).toLowerCase(), vnode.props[key]);
    }
  }
  // 处理 children
  if (typeof vnode.children === 'string') {
    // 文本直接添加
    el.appendChild(document.createTextNode(vnode.children))
  } else if (Array.isArray(vnode.children)) {
    // 递归渲染子节点
    vnode.children.foreach(child => renderer(child, el))
  }
  container.appendChild(el);
}

const renderer = (vnode, root) => {
  // tag 类型为 string 说明是普通的标签
  if (typeof vnode.tag === 'string') {
    mountElement(vnode, root);
    // 为 function 说明是组件,这里也可以为 object
  } else if (typeof vnode.tag === 'function') {
    const componentVnode = vnode.tag()
    // 递归渲染
    renderer(componentVnode, root);
  }
}

上面我们就简单实现了一个将虚拟DOM转换为真实DOM的方法,这就是渲染器,当然 Vue 的渲染器要比这个复杂的多!

通过上面的介绍,我们其实可以看到 Vue 的工作流程实际上是这样的:

最后

本文只是对 Vue 的简单一瞥,主要是对 Vue 的整个设计思路有一个大致的了解,后续的文章将会对每一个模块进行详细地介绍。