Skip to content

Hello Vue3

相比与之前的原生 JS 与 Jquery 项目经验。构建 Vue 项目(现代化 Web 项目基本如此)时需要从根本上改变一些思维习惯。本文除了介绍 Vue 的基础上手使用,更想通过一个简单的 Vue 例子来实现这种思维方式的过渡。

Vue.js 是什么

先看官方文档的介绍:

Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。另一方面,当与现代化的工具链以及各种支持类库结合使用时,Vue 也完全能够为复杂的单页应用提供驱动。

从设计的角度来看,Vue 能够涵盖下图所有的东西。实际上并不需要一上手就把所有东西全用上,因为没有必要。完全可以根据自己的实际需求,灵活、逐步采用。

声明式渲染组件系统是Vue的核心库。而客户端路由、状态管理、构建工具都有专门解决方案。这些解决方案相互独立,可以在核心的基础上任意选用其他的部件,不一定要全部整合在一起。

Hello Vue3

Vue 的上手非常简单,由于其渐进式特点且其核心库只关注视图层,非常方便与其他库或既有项目整合。比如我们在 Jquery 项目基础上直接使用Vue去渲染我们的 Hello vue3 应用。只需要:

MVVM模式

MVVM 是Model-View-ViewModel的简写。它本质上就是 MVC 的改进版。MVVM 模式中有三个核心组件:模型、视图和视图模型。下图展示了这三个组件之间的关系。

  • 视图:定义用户在屏幕上看到的内容的结构、布局和外观
  • 模型:封装业务逻辑与应用数据
  • 视图模型:视图模型将视图所需的数据与模型提供的数据进行绑定,并监听视图状态的变更以更新模型数据以及监听模型数据的变化以同步更新视图的状态

Vue 的 核心库只关注视图层,虽然没有完全遵循 MVVM 模型,但是 Vue 的设计也受到了它的启发。

上面的例子中 Vue 通过 mount 方法返回一个 vm (ViewModel 的缩写)实例。

js
// 最后将 Vue 应用实例渲染到 Dom
const vm = app.mount("#app");

Vue渲染机制

DOM 树

浏览器会将HTML代码解析为一个 "DOM 节点" 树来保持追踪所有内容。每个元素都是一个节点。每段文字也是一个节点。甚至注释也都是节点。一个节点就是页面的一个部分。每个节点都可以有孩子节点 (也就是说每个部分可以包含其它的一些部分)。

html
<div>
  <h1>My title</h1>
  Some text content
  <!-- TODO: Add tagline -->
</div>
可以使用原生 js 或者 Jquery 直接操作 DOM 节点来更新页面所展示的内容。但是每次操作 DOM 节点浏览器都会立即开始重新渲染页面。所造成的性能开销是很大的。也因此,虚拟 DOM 应运而生。

虚拟 DOM

虚拟 DOM (Virtual DOM ) 是一种编程概念,是对"真实" DOM 的一种抽象。这个概念是由 React 开创的,并且已经在许多其他具有不同实现的框架中进行了调整,包括 Vue。

虚拟 DOM 是轻量级的 JavaScript 对象。它抽象了我们创建实际元素所需的所有信息。一般至少包含三个参数:元素,具有数据、prop、attr 等的对象,以及一个 children 数组。children 数组是我们传递子级的地方,子级也具有所有这些参数,然后它们也可以具有子级,依此类推,直到构建完整的元素树为止。

HTML代码:
html
<div id="vueapp">
  <img src="assets/vuelogo.png"></img>
  <h1 style="font-size: 32px;">Hello Vue3</h1>
</div>
虚拟 DOM 树:
js
const vnode = {
  tag: "<div>",
  props: {"id": "vueapp"},
  children: [
    {tag: "<img>", props: {"src": "assets/vuelogo.png"}, children: []},
    {tag: "<h1>", props: { "style": {fontSize: "32px"}}, children: ["Hello Vue3"]}
  ]
}

虚拟 DOM 使开发人员能够以编程方式创建、检查和组合所需的 UI 结构。当需要批量更新 DOM 时,可以先更新虚拟 DOM 树(实际就是在内存中 JavaScript 对象)。然后在它们和实际 DOM 之间执行 diff。最后一次性更改真实 DOM。

Vue 渲染机制

首先 Vue 的编译器将组件模板编译成渲染函数。然后调用渲染函数生成虚拟 DOM 树。再交给一个 patch 函数,负责把这些虚拟 DOM 真正施加到真实的 DOM 上。在这个过程中,Vue 利用自身的响应式系统来收集并侦测在渲染过程中所依赖到的数据来源。侦测到的数据源的变动后会重新进行渲染,生成一个新的树,将新树与旧树进行对比,就可以最终得出应施加到真实 DOM 上的改动。最后再通过 patch 函数施加改动。

下面以前面的 Hello Vue3 的例子简述整个过程:

模板编译

模板就是 HTML 页面布局和外观。Vue 使用了基于 HTML 的模板语法,允许声明式地将 DOM 绑定至底层组件实例的数据。在底层的实现上,Vue 将模板编译成虚拟 DOM 渲染函数。事实上 Vue 编程也可以不用模板,直接写渲染函数,甚至结合 Babel 插件,使用 React 的 JSX 语法。

虽然直接写渲染函数可以更灵活并具有 JavaScript 的完全编程的能力等优势,但 Vue 推荐在绝大多数情况下使用模板来创建你的 HTML。主要有以下两个原因:

  • 模板语法更接近于真实的 HTML。可以重用现有的HTML,CSS等相关最佳实践技术栈,便于设计人员理解与修改。
  • 由于模板更具确定性,因此更易于静态分析。 可以让 Vue 的模板编译器应用许多编译时优化来提高虚拟 DOM 的性能。

在实践中,模板足以满足应用程序中的大多数用例。 渲染函数通常仅用于需要处理高度动态渲染逻辑的可重用组件。

组件模板:
html
<div id="vueapp">
  <img :src="imgsrc" />
  <h1
    @mouseover="changeFontSize"
    @mouseout="changeFontSize"
    :style="styleObject"
  >
    {{ message }}
  </h1>
</div>
渲染函数:
js
export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock("div", { id: "vueapp" }, [
    _createElementVNode("img", { src: _ctx.imgsrc }, null, 8 /* PROPS */, ["src"]),
    _createElementVNode("h1", {
      onMouseover: _ctx.changeFontSize,
      onMouseout: _ctx.changeFontSize,
      style: _normalizeStyle(_ctx.styleObject)
    }, _toDisplayString(_ctx.message), 45 /* TEXT, STYLE, PROPS, HYDRATE_EVENTS */
    , ["onMouseover", "onMouseout"])
  ]))
}

调用渲染函数生成虚拟 DOM 树并渲染到真实 DOM

虚拟 DOM 树:
js
const vnode = {
  tag: "<div>",
  props: {"id": "vueapp"},
  children: [
    {tag: "<img>", props: {"src": "assets/vuelogo.png"}, children: []},
    {tag: "<h1>", props: { "style": {fontSize: "32px"}}, children: ["Hello Vue3"]}
  ]
}
真实 DOM:

鼠标移入移出时改变 Hello Vue3 依赖的 "fontSize" 的值,响应式系统监听 "fontSize" 的变化重新生成新的虚拟 DOM 树并比较前后的虚拟 DOM 树,只渲染有变化的 DOM

比较前后的虚拟 DOM 树:
js
const vnode = {
  tag: "<div>",
  props: {"id": "vueapp"},
  children: [
    {tag: "<img>", props: {"src": "assets/vuelogo.png"}, children: []},
    {tag: "<h1>", props: { "style": {fontSize: "32px"}}, children: ["Hello Vue3"]}
  ]
}
js
const vnode = {
  tag: "<div>",
  props: {"id": "vueapp"},
  children: [
    {tag: "<img>", props: {"src": "assets/vuelogo.png"}, children: []},
    {tag: "<h1>", props: {"style": {fontSize: "48px"}}, children: ["Hello Vue3"]}
  ]
}
真实 DOM:

构建 Vue 项目的基本思想

其实不单单是 Vue 项目,基本上所有的现代化框架在构建 Web 项目时都遵循以下几点。

组件化

基本上所有的现代框架都已经走向了组件化道路,Web Components 从规范层面做这个实践。主流框架都有各有不同的封装,但核心思想都是一样,把 UI 结构映射到恰当的组件树,如下图所示。

DOM 状态只是数据状态的一个映射

现在基本所有的框架都已经认同这个看法——DOM 应尽可能是一个函数式到状态的映射。状态即是唯一的真相,而 DOM 状态只是数据状态的一个映射。在构建项目时,关注点在于状态的管理,所有的逻辑尽可能在状态的层面去进行,当状态改变的时候,View 会在框架帮助下自动更新到合理的状态。

下面是一个单向数据流概念的简单表示: State 驱动 View 的渲染,而用户对 View 进行操作产生 Action,会使 State 产生变化,从而导致 View 重新渲染。

一个单独的Vue的组件,其实就已经是这样的结构。比如前面的例子中 Hello Vue3 的字体大小变化。我们需要做的是:

  • 模板中绑定 fontSize 的数据以及数据变化的 Action
html
<h1
  @mouseover="changeFontSize"
  @mouseout="changeFontSize"
  :style="styleObject"
>  {{ message }} </h1>
  • 根据实际业务需要变更 fontSize 的数据状态
js
// 首先定义组件
const helloVue = {
  data() {
    return {
      message: "Hello Vue3",
      imgsrc: "assets/vuelogo.png",
      styleObject: {
        fontSize: "32px"
      }
    };
  },
  methods: {
    changeFontSize(e) {
      this.styleObject.fontSize = e.type === "mouseover" ? "48px" : "32px";
    }
  }
}

至于 View 的状态变化则由 Vue 框架帮我们自动更新。

状态管理

组件是独立可复用的,每个组件都有它自己的状态。对于大型应用,当多个组件来配套的时候,状态管理将变得复杂。

  • 多个视图可能依赖于同一个状态
  • 多个不同视图的操作可能需要改变相同的状态

不同的框架都有各自的解决方案。大部分解决方案是把这个状态从组件树中提取出来,放在一个全局的 Store 里面。Vue 提供 Vuex 专门负责大型应用的状态管理。

参考文章