likes
comments
collection
share

Vue.js 3.0 一些优化总结

作者站长头像
站长
· 阅读数 14

目的

  1. 了解Vue.js 3.0 的升级给我们开发带来的收益
  2. 学习到一些设计思想和理念,并在自己的开发工作中应用以获得提升

Vue3会带来些什么?

  • 更快:重构虚拟dom(编译优化),Proxy响应式对象
  • 更小:Tree shaking
  • 更易于维护:TS、代码管理和Composition API

源码优化

源码的优化主要体现在使用 monorepo 和 TypeScript 管理和开发源码,提升了代码可维护性。具体变化如下:

更好的代码管理方式:monorep

Vue.js 2.x 的源码托管在 src 目录,然后依据功能拆分出了:

  • compiler(模板编译的相关代码)
  • core(与平台无关的通用运行时代码)
  • platforms(平台专有代码)
  • server(服务端渲染的相关代码)
  • sfc(.vue 单文件解析相关代码)
  • shared(共享工具代码)

 Vue.js 3.0 一些优化总结

Vue.js 3.0整个源码是通过 monorepo 的方式维护的,根据功能将不同的模块拆分到 packages 目录下面不同的子目录中:

 Vue.js 3.0 一些优化总结

模块被拆分到不同的 package 中,每个 package 有各自的 API、类型定义和测试。这样做的好处有以下几点:

  • 模块拆分更细化,职责划分更明确
  • 模块之间的依赖关系也更加明确
  • 开发人员也更容易阅读、理解和更改所有模块源码,提高代码的可维护性
  • 一些 package(reactivity响应式库等)是可以独立于 Vue.js 使用的。可单独依赖无需引用整个Vue.js

TypeScript

JavaScript 是动态类型语言,它的灵活性有目共睹,但是过于灵活的副作用是很容易就写出非常隐蔽的隐患代码,在编译期甚至看上去都不会报错,但在运行阶段就可能出现各种奇怪的 bug。

对于复杂的框架项目开发,使用类型语言非常有利于代码的维护,因为它可以在编码期间帮你做类型检查,避免一些因类型问题导致的错误。也可以去定义类和接口,便于IDE进行类型推断。

Vue.js 3.0 自身采用了 TypeScript 进行开发。Vue2.x采用FaceBook出品的 JavaScript 静态类型检查工具Flow进行管理。但是 Flow 对于一些复杂场景类型的检查,支持得并不好,例如如下代码:

const propOptions: any = vm.$options.props 

Flow 无法正确推导出 vm.$options.props 的类型,就不得不使用any进行强制声明。另外,社区里有人吐槽过Flow团队存在烂尾的可能。而TS有两个明显的优势:

  • 有更好的类型检查,能支持复杂的类型推导
  • TypeScript 的生态更加成熟和活跃,生命力更强

性能优化

源码体积优化

Vue.js 3.0 为了减少源码体积:

  • 移除一些冷门的 feature
  • 引入 tree-shaking 的技术,减少打包体积。

一些不常用的feature,例如filter

<template>
    <div>{{millisecond | format}}</div>
</template>
<script>
export default {
    props : {
	millisecond : 1516101106
    },
  filter: {
    format(value) {
	if (!value) return ''
	value = value.toString()
	return value.charAt(0).toUpperCase() + value.slice(1)
    }
  }
}

// 3.x 中,过滤器已删除,不再支持。可以使用计算属性或调用方法替换他们
<template>
  <h1>{{ format }}</h1>
</template>
<script>
export default {
	props : {
            millisecond : 1516101106
	},
        computed: {
            format(value) {
		if (!value) return ''
		value = value.toString()
		return value.charAt(0).toUpperCase() + value.slice(1)
        }
  }
}
</script>

再来看看 tree-shaking。它的原理很简单:tree-shaking 依赖静态的 ES2015 模块化语法(import  和 export 导入导出)。通过编译阶段的静态分析,找到没有引入的模块并打上标记,之后在压缩阶段删除掉那些没用的代码。例如,有如下工具类util.js:

export function funcA() {
	//do something
}

export function funB() {
	//do something
}

我们在main.js里引用它:

import {funcA} from './util.js';
funcA();

使用了tree-shaking之后最终打包后的util.js会变成下面的模样:

export function funcA() {
}

数据劫持优化

Vue3.0之前是通过Object.defineProperty 劫持数据的 getter 和 sette:

Object.defineProperty(data, 'a',{
  get(){
    // track
  },
  set(){
    // trigger
  }
})

通过Object.defineProperty使普通对象变为响应式对象,核心原理就是在getter中收集依赖,在sette中触发事件。但是,该方法有个缺点:在进行劫持之前必须知道所要拦截的属性key是什么,因此,它不无法拦截对象的属性的增加和删除。用如下代码举例:

<div id="app">
    <p>{{message.a}}</p>
    <p>{{message.b}}</p>
</div>
<script>
    const app = new Vue({
        el: "#app",
        data: {
            message: {
                a:""
            }
        },
        mounted() {
            //this.message.a = "A"
            this.message.b = "B"
            //this.$set(this.message,"b", "B")
        }
    })
</script>

如果单独使用 this.message.b = "B" 是无法触发页面更新的,Vue.js 为了解决这个问题提供了 set和set 和 setdelete 实例方法。但是这个明显给开发者带来了代码维护上的负担。

另外在面对嵌套比较深的对象时,Object.defineProperty需要通过递归遍历这个对象,把每一层的对象数据都变成响应式的,删减完整源码后大致如下

export function observe (value: any, asRootData: ?boolean): Observer | void {
  ob = new Observer(value)
  return ob
}

export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that has this object as root $data

  constructor (value: any) {
    this.walk(value)
  }

  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }
}

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {

  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      //...
    },
    set: function reactiveSetter (newVal) {
      //...
    }
  })
}

defineReactive函数拿到 obj的属性描述符,然后对子对象递归调用 observe方法,这样就保证了无论 obj的结构有多深,它的所有子属性也能变成响应式的对象,这样无论我们修改和使用任何属性,都能触发 getter 和 setter。最后利用 Object.defineProperty去给 obj 的属性 key 添加 getter 和 setter。毫无疑问——如果定义的响应式数据过于复杂,这就会有相当大的性能负担。

Vue3.0 使用了 Proxy API 做数据劫持,其官方定义如下:

  • Proxy对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找、赋值、枚举、函数调用等)。

它劫持的是整个对象,实现结构结构如下:

observed = new Proxy(data, {
  get() {
    // track
  },
  set() {
    // trigger
  }
})

然而,虽然对于对象的属性的增加和删除Proxy都能检测到,但是并不能监听到内部深层次的对象变化,VUE3采用的方式依然是递归。但是,是在 getter 中去递归响应式,这样只有真正被访问到的内部对象才会变成响应式,而不是无差别全部递归。完整源码删减后如下:

function createGetter(isReadonly = false) {
  return function get(target, key, receiver) {
    // 求值
    const res = Reflect.get(target, key, receiver)
    // 依赖收集
    !isReadonly && track(target, "get" /* GET */, key)
    if (isObject(res)) {
      // Convert returned value into a proxy as well. we do the isObject check
      // here to avoid invalid value warning. Also need to lazy access readonly
      // and reactive here to avoid circular dependency.
      return isReadonly ? readonly(res) : reactive(res)
    }
    return res
  }
}

另外,Vue3使用了ref/reactive 替换了data中的变量。这样不仅使得变量定义更加灵活多变,而且避免了不必要的数据被转换为响应式数据。

语法优化:Composition API

Vue3之前,一直使用的是Options API,可以认为我们编写组件的工作就是对组件的配置项进行描述。 通过按照 methods、computed、data、props 等这些不同的选项进行分类,当组件小的时候,这种分类方式一目了然这样编写的代码简单直观,易于上手。

<script>
export default {
    data() {
        return {
            balance: 0,
            amount: 0
        }
    },
    computed: {
        balanceString() {
            return `Account Balance: ${this.balance}`;
        }
    },
    methods: {
        addBalance() {
            this.balance += this.amount
            this.amount = 0;
        },
        subtractBalance() {
            this.balance -= this.amount
            this.amount = 0;
        }
    },
    mounted() {
        console.log('Application mounted');
    },
}
</script>

但是在较大的组件中,可能包含有多个逻点,每一个关注点的相关代码分散在不同的配置项中,当想要需要修改一个逻辑时,就需要在单个文件中不断上下切换和寻找。Composition API的优化项之一就是解决这个问题。

Compositions API 提供Options API 欠缺的两种能力:

  • 将相关代码片段组合在一起
  • 可组合型使得复用代码更加的轻松方便

下面是使用了Compositions API的一段代码

<script setup>
import { onMounted, ref } from 'vue';

const amount = ref(0);
const balance = ref(0);

const balanceString = computed(() => `Account Balance: ${balance.value}`);

const addBalance = () => {
    balance.value += amount.value;
    amount.value = 0;
}

const subtractBalance = () => {
    balance.value -= amount.value;
    amount.value = 0;
}

onMounted(() => {
    console.log('Application mounted');
});
</script>

在业务量较小时,无法直观的体现出组合式Api的优势。我们用蓝黄橙三色分别表示三个业务点,下图可以直观的反映出二者在编码上的不同:

 Vue.js 3.0 一些优化总结

其次,是优化逻辑复用。假如我们需要在组件内引入一个人员管理数据模块,在Vue2中可以通过mixin实现,首先编写如下代码:

const userList = {
  data() {
    return {
      users: ['HU',"Liao","SI","ZHOU","DENG","WEI"]
    }
  },
  methods: {
    list() {
      return this.users
    },
    addUser(user) {
      this.users.add(user)
    },
    removeUser(user) {
      this.users.remove(user)
    }
  }
}
export default userList

然后在组件中使用:

<template>
  <div>
    All user:{{ list }}
  </div>
</template>

<script>

import userListMixin from './userList'

export default {
  mixins: [userListMixin]
}

</script>

虽然使用起来非常方便,但是这种方法有两个问题:

  • 命名冲突 :每个mixin 都可以定义自己的 props、data,它们之间是没有关联的,很容易定义相同的变量,导致命名冲突
  • 数据来源不清晰:在模板中使用不在当前组件中定义的变量,不太容易知道这些变量在哪里定义

使用Compositions API 来实现上述功能:

export const useUserList = () => {
    const list = ref('HU',"Liao","SI","ZHOU","DENG","WEI"]);
    const addUser = (user) => {
        list.value.push(user);
    };
    const remove = (user) => {
        list.value.remove(user);
    };

    return {
        list,
        addUser,
        removeUser,
    };
};
<template>
  <div>
    All user : {{ users }}
  </div>
</template>
<script>
  import useMousePosition from './useUserList'
  export default {
    setup() {
      const { list:users } = useUserList()
      return { users }
    }
  }
</script>
</script>

整个数据来源清晰了,还可以通过解构任意命名,可以有效的避免命名冲突和数据来源不明确的问题。

最后,总结一下Composition API相对与Options API的优势:

  • 有助于编写干净的代码,使得代码的可读性更高
  • 可以将同一逻辑的代码片段组合在一起
  • 让代码复用变的更轻松方便