Vue.js 3应用开发与核心源码解析
上QQ阅读APP看书,第一时间看更新

1.4.1 更快、更小、更易于维护

1.更快

更快主要体现在Vue 3在性能方面的提升,以及在源码层面的改动,主要包括以下方面:

· 重构虚拟DOM。

· 事件缓存。

· 基于Proxy的响应式对象。

(1)重构虚拟DOM

Vue 3重写了虚拟DOM的实现方法,使得初始渲染/更新可以提速达100%,对于Vue 2.x版本的虚拟DOM来说,Vue会遍历<template>模板中的所有内容,并根据这些标签生成对应的虚拟DOM(虚拟DOM一般指采用key/value对象来保存标签元素的属性和内容),当有内容改变时,遍历虚拟DOM来找到变化前后不同的内容,我们称这个过程叫作diff(different),并找到针对这些变化的内容所对应的DOM节点,并改变其内部属性。例如下面这段代码:

     <template>
       <div class="content">
         <p>number1</p>
         <p>number2</p>
         <p>number3</p>
         <p>{{count}}</p>
      </div>
     </template>

当触发响应式时,遍历所有的<div>标签和<p>标签,找到{{count}}变量对应的<p>标签的DOM节点,并改变其内容。对于那些纯静态<p>标签的节点进行diff其实是比较浪费资源的,当节点的数量很少时,表现并不明显,但是一旦节点的数量过大,在性能上就会慢很多。对此,Vue 3在此基础上进行了优化,主要有:

· 标记静态内容,并区分动态内容(静态提升)。

· 更新时只diff动态的部分。

针对上面的代码,Vue 3中首先会区分出{{count}}这部分动态的节点,在进行diff时,只针对这些节点进行,从而减少资源浪费,提升性能。

(2)事件缓存

我们知道在Vue 2.x中,在绑定DOM事件时,例如@click,这些事件被认为是动态变量,所以每次更新视图的时候都会追踪它的变化,然后每次触发都要重新生成全新的函数。在Vue 3中,提供了事件缓存对象cacheHandlers,当cacheHandlers开启的时候,@click绑定的事件会被标记成静态节点,被放入cacheHandlers中,这样在视图更新时也不会追踪,当事件再次触发时,就无须重新生成函数,直接调用缓存的事件回调方法即可,在事件处理方面提升了Vue的性能。

未开启cacheHandlers编译后的代码如下:

     <div @click="hi">Hello World</div>
     
     // 编译后
     export function render(_ctx, _cache, $props, $setup, $data, $options) {
         return (_openBlock(), _createElementBlock("div", { onClick: _ctx.hi }, "Hello World1",
8 /* PROPS */, ["onClick"]))
      }

开启cacheHandlers编译后的代码如下:

     <div @click="hi">Hello World</div>
     
     // 编译后
     export function render(_ctx, _cache, $props, $setup, $data, $options) {
       return (_openBlock(), _createElementBlock("div", {
         onClick: _cache[0] || (_cache[0] = (...args) => (_ctx.hi && _ctx.hi(...args)))
       }, "Hello World1"))
     }

可以看到主要区别在于onClick那一行,直接从缓存中读取了回调函数。

(3)基于Proxy的响应式对象

在Vue 2.x中,使用Object.defineProperty()来实现响应式对象,对于一些复杂的对象,需要循环递归地给每个属性增加getter/setter监听器,这使得组件的初始化非常耗时,而Vue 3中,引入了一种新的创建响应式对象的方法reactive,其内部就是利用ES 6的Proxy API来实现的,这样就可以不用针对每个属性来一一进行添加,以减少开销,提升性能。我们会在后续章节具体讲解Vue 3的响应式和Proxy API。

2.更小

更小主要体现在包所占容量的大小,我们知道,前端资源一般都属于静态资源,例如JavaSript文件、HTML文件等,这些资源都托管在服务器上,用户在使用浏览器访问时,会将这些资源下载下来,所以精简文件包大小是提升页面性能的重要因素。Vue 3在这方面可以让开发者打包构建出来的资源更小,从而提升性能。

Tree Shaking是一个术语,通常用于描述移除JavaScript上下文中的未引用代码(dead-code),就像一棵大树,将那些无用的叶子都剪掉。它依赖于ES 6模块语法的静态结构特性,例如import和export,这个术语和概念在打包工具Rollup和Webpack中普及开来。例如下面这段ES 6代码:

     import {get} from './api.js'
     
     let doSome = ()=>{
         get()
     }
     
     doSome()
     
     // api.js
     let post = ()=>{
         console.log('post')
     }
     
     export post
     let get = ()=>{
         console.log('get')
     }
     
     export get

上面的代码中,api.js代码中的post方法相关内容是没有被引入和使用的,有了Tree Shaking之后,这部分内容是不会被打包的,这就在一定程度上减少了资源的大小。使用Tree Shaking的原理是引入了ES 6的模块静态分析,这就可以在编译时正确判断到底加载了什么代码,但是要注意import和export是ES 6原生的,而不是通过Babel或者Webpack转化的。

在Vue 3中,对代码结构进行了优化,让其更加符合Tree Shaking的结构,这样使用相关的API时,就不会把所有的都打包进来,只会打包用户用到的API,例如:

     <!-- vue 2.x -->
     import Vue from 'vue'
     
     new Vue()
     
     Vue.nextTick(() => {})
     
     const obj = Vue.observable({})
     
     <!-- vue 3.x -->
     import { nextTick, observable,createApp } from 'vue'
     
     
     nextTick(() => {})
     
     const obj = observable({})
     
     createApp({})

同理,例如<keep-alive>、<transition>和<teleport>等内置组件,如果没有使用,也不会被打包到资源中。

3.更易于维护

(1)从Flow迁移到TypeScript

TypeScript是微软开发的一个开源的编程语言,通过在JavaScript的基础上添加静态类型定义构建而成,其通过TypeScript编译器或Babel转译为JavaScript代码,可运行在任何浏览器和操作系统上。TypeScript引入了很多新的特性,例如类型监测、接口等,这些特性在框架源码的维护上有很大的提升。

在Vue 3的源码结构层面,从Flow改成了TypeScript来编写,Flow是一个静态类型检测器,有了它就可以在JavaScript运行前找出常见的变量类型的bug,类似于Java语言中给变量强制指定类型,它的功能主要包括:

· 自动类型转换。

· null引用。

· 处理undefined is not a function。

例如:

     // @flow
     
     function foo(x: number): number {
       return x + 10
     }
     
     foo('hi') // 参数x须为number类型,否则会报错
     
     错误信息:
     '[flow] string (This type is incompatible with number See also: function call)'

上面这段代码采用了Flow后,如果类型不对就会报错。一般来说,对于JavaScript源码框架,引入类型检测是非常重要的,不仅可以减少bug的产生,还可以规范一些接口的定义,这些特性和TypeScript非常吻合,所以在Vue 3中直接采用了TypeScript来进行重写,从源码层面来提升项目的可维护性。

(2)源代码目录结构遵循Monorepo

Monorepo是一种管理代码的方式,它的核心观点是所有的项目在一个代码仓库中,但是代码分割到一个个小的模块中,而不是都放在src这个目录下。这样的分割,使得每个开发者大部分时间只是工作在少数的几个文件夹内,并且也只会编译自己负责的模块,不会导致一个IDE打不开太大的项目之类的事情,这样很多事情就简单了很多。Monorepo的结构如图1-3所示。

图1-3 Monorepo的结构

目前很多大型的框架(例如Babel、React、Angular、Ember、Meteor、Jest等)都采用了Monorepo这种方式来进行源码的管理,当然在自己的业务项目中,也可以使用Monorepo来管理代码。我们可以看一下Vue.js在采用Monorepo前后的源码结构对比,如图1-4所示。

图1-4 Vue 2.x源码目录结构(左)和Vue 3.x源码目录结构(右)