vue3对比vue2
接下来,我们从分析 Vue2 优缺点入手,以及结合图片和用例来了解 Vue3 的优势。
vue2
优点
1.在 Github 的 Front-End 分类的排行榜上能看到 Vue 仓库的排名是第一,粉丝足够多,活跃用户足够多,当然相应的生态相对强大。
2.响应式数据,Options API,SFC,Vuex,Vue-Router,以及丰富的周边插件和活跃的社区。
缺点
在一个组件仅承担单一逻辑的时候,使用 Options API 来书写组件是很清晰的。 但是在我们实际的业务场景中,一个父组件总是要包含多个子组件,父组件需要给子组件传值、处理子组件事件、直接操作子组件以及处理各种各样的调接口的逻辑,这时候我们的父组件的逻辑就会变得复杂。 我们从代码维护者的角度出发,假设这个时候有 10 个函数方法,自然我们要把他们放入到methods中,而这个 10 个函数方法又分别操作 10 个数据,这个 10 个数据又分别需要进行Watch操作。 这时候,我们根据Vue2的Options API的写法,就写完了 10 个method、10 个data、10 个watch,我们就将本来 10 个的函数方法,分割在 30 个不同的地方。 这时候父组件的代码,对代码维护者来说是很不友好的。
vue3
优点
- 更强的性能,更好的 tree shaking
- Composition API + setup
- 更好地支持 TypeScript
- diff算法重写(最长递增子序列替代vue2的双端比较)
💡 vue2diff算法:定义四个指针,分别比较
- oldStartNode 和 newStartNode
- oldStartNode 和 newEndNode
- oldEndNode 和 newStartNode
- oldEndNode 和 newEndNode
💡 vue3diff算法: 例如数组 [3,5,7,1,2,8] 的最长递增子序列就是 [3,5,7,8 ] 。这是一个专门的算法。 算法步骤:
- 通过“前-前”比较找到开始的不变节点
[A, B]
- 通过“后-后”比较找到末尾的不变节点
[G]
- 剩余的有变化的节点
[F, C, D, E, H]
- 通过
newIndexToOldIndexMap
拿到 oldChildren 中对应的 index[5, 2, 3, 4, -1]
(-1
表示之前没有,要新增)- 计算最长递增子序列得到
[2, 3, 4]
,对应的就是[C, D, E]
,即这些节点可以不变- 剩余的节点,根据 index 进行新增、删除
该方法旨在尽量减少 DOM 的移动,达到最少的 DOM 操作
缺点
可能存在一个问题:那就是如何优雅地组织代码?代码不能优雅的组织,在代码量上去之后,也一样很难维护。
SFC写法变化
💡Vue2 一个普通的 Vue 文件
<template>
<div>
<p>{{ person.name }}</p>
<p>{{ car.name }}</p>
</div>
</template>
<script>
export default {
name: "Person",
data() {
return {
person: {
name: "小明",
sex: "male",
},
car: {
name: "宝马",
price: "40w",
}
};
},
watch:{
'person.name': (value) => {
console.log(`名字被修改了, 修改为 ${value}`)
},
'person.sex': (value) => {
console.log(`性别被修改了, 修改为 ${value}`)
}
},
methods: {
changePersonName() {
this.person.name = "小浪";
},
changeCarPrice() {
this.car.price = "80w";
}
},
};
</script>
采用 Vue3 Composition API 进行重写
<script lang="ts" setup>
// person的逻辑
const person = reactive<{ name: string; sex: string }>({
name: "小明",
sex: "male",
});
function changePersonName() {
person.name = "小浪";
}
// car的逻辑
const car = reactive<{ name: string; price: string }>({
name: "宝马",
price: "40w",
});
function changeCarPrice() {
car.price = "80w";
}
watch(
() => [person.name, person.sex],
([nameVal, sexVal]) => {
console.log(`名字被修改了, 修改为 ${nameVal}`);
console.log(`名字被修改了, 修改为 ${sexVal}`);
}
);
</script>
<template>
<p>{{ person.name }}</p>
<p>{{ car.name }}</p>
</template>
采用 Vue3 自定义 Hook 的方式,进一步拆分
<script lang="ts" setup>
import { usePerson, useCar, useAnimal } from "./hooks";
const { person, changePersonName } = usePerson();
const { car } = useCar();
</script>
<template>
<p>{{ person.name }}</p>
<p>{{ car.name }}</p>
<p>{{ animal.name }}</p>
</template>
// usePerson.ts
import { reactive, watch } from "vue";
export default function usePerson() {
const person = reactive<{ name: string; sex: string }>({
name: "小明",
sex: "male",
});
watch(
() => [person.name, person.sex],
([nameVal, sexVal]) => {
console.log(`名字被修改了, 修改为 ${nameVal}`);
console.log(`名字被修改了, 修改为 ${sexVal}`);
}
);
function changePersonName() {
person.name = "小浪";
}
return {
person,
changePersonName,
};
}
// useCar.ts
import { reactive } from "vue";
export default function useCar() {
const car = reactive<{ name: string; price: string }>({
name: "宝马",
price: "40w",
});
function changeCarPrice() {
car.price = "80w";
}
return {
car,
changeCarPrice,
};
}
对比完之后,我们会发现,Vue3 可以让我们更好组织代码。person和car的逻辑都被单独放置在一块
- Watch也不需要罗列多个了,Vue3中支持侦听多个源
- 很自然的使用TypeScript,类型约束更加简单
-
p {
// 使用顶层绑定
color: v-bind('theme.color');
}
### 使用指令 ```vue <script setup> import { ref } from 'vue' const total = ref(10) // 自定义指令 // 必须以 小写字母v开头的小驼峰 的格式来命名本地自定义指令 // 在模板中使用时,需要用中划线的格式表示,不可直接使用vMyDirective const vMyDirective = { beforeMount: (el, binding, vnode) => { el.style.borderColor = 'red' }, updated(el, binding, vnode) { if (el.value % 2 !== 0) { el.style.borderColor = 'blue' } else { el.style.borderColor = 'red' } }, } const add = () => { total.value++ } </script> <template> <input :value="total" v-my-directive /> <button @click="add">add+1</button> </template>
获取组件实例
在 vue2 中,我们用 this 访问组件的实例,在 vue3 的 setup() 选项或
<script setup> import { getCurrentInstance, ref, onMounted } from 'vue' import HelloWorld from './components/HelloWorld.vue' // 获取当前组件的实例对象,相当于 vue2 的 this const instance = getCurrentInstance() console.log(instance) // 获取 HelloWorld 组件实例 const helloWorld = ref(null) onMounted(() => { console.log(instance.$el) console.log(helloWorld.value.$el) helloWorld.value.$el.setAttribute('data-name', '定义name'); }) </script> <template> <div>标题</div> <HelloWorld title="11" ref="helloWorld" /> </template>
Pinia
Pinia是一个全新的Vue状态管理库,其实pinia就是vuex5,为了尊重pinia作者,库名不做修改。
- Vue2 和 Vue3 都能支持
- 抛弃传统的 Mutation ,只有 state, getter 和 action ,简化状态管理库
- 不需要嵌套模块,符合 Vue3 的 Composition api,让代码扁平化
- TypeScript支持
- 代码简介,很好的代码自动分割
- 轻量级只有1-2kb
创建Store
//src/store/user.ts import { defineStore } from 'pinia' export const userStore = defineStore({ id: 'user', // id必填,且需要唯一 或者‘user’ state: () => { return { name: '张三', count:5 } } })
获取State
<script lang="ts" setup> import { userStore } from '@/store/user' const store = userStore() </script> <template> <div>{{ store.name }}</div> </template>
解构store 当store中的多个参数需要被使用到的时候,为了更简洁的使用这些变量,我们通常采用结构的方式一次性获取所有的变量名,ES传统的解构(能取到值,但是数据不具有响应式) pinia的解构方法(storeToRefs)
<script lang="ts" setup> import { userStore } from '@/store/user' import { storeToRefs } from 'pinia' const store = userStore() const {count} = storeToRefs(store) </script> <template> <div>{{ store.name }}</div> <div>{{ count }}</div> </template>
pinia修改多条数据 通过基础数据修改方式去修改多条数据也是可行的,但是在 pinia 官网中,已经明确表示patch $patch 方法可以接受两个类型的参数,函数 和 对象
- $patch + 对象
- $patch + 函数: 通过函数方式去使用的时候,函数接受一个 state 的参数,state 就是 store 仓库中的 state
<script lang="ts" setup> import { userStore } from '@/store/user' import { storeToRefs } from 'pinia' const store = userStore() // $patch + 对象 const onObjClick = ()=> { store.$patch({ count:store.count + 8 name:store.name === '张三' ? 'hello world' : '张三' }) } // $patch + 函数 const onObjClick = ()=> { store.$patch((state)=>{ count:state.count + 8 name:state.name === '张三' ? 'hello world' : '张三' }) } </script> <template> <button @click="onObjClick"></button> <button @click="onFunClick"></button> </template>
store之间的相互调用 在 Pinia 中,可以在一个 store 中 import 另外一个 store ,然后通过调用引入 store 方法的形式,获取引入 store 的状态
- 新建 store
// src/store/allan-ts import { defineStore } from'pinia' export const allanStore = defineStore('allan',{ state: () => { return { moveList:['泰坦尼克号!,’绿皮书!, 肖申克的救赎",,阿甘正传!,“星际穿越"] } }, getters: {}, actions:{} })
- 在原 store 中引入 allanStore,并获取 moveList
// src/store/allan-ts import { defineStore } from'pinia' import { allanStore } from'./allan' export const allanStore = defineStore('main',{ state: () => { return { msg:'hello world', count:0 } }, getters: { // 另一个Store引用 获取allanStore中的moveList getAllanStoreList():string[] { return allanStore().moveList } }, actions:{} })
pnpm
和npm,yarn一样,pnpm是一个包管理工具。不一样的是,pnpm解决了npm和yarn一直都没有解决的痛点。在许多方面比npm和yarn更优秀。 当使用 npm 或 Yarn 时,如果你有 100 个项目,并且所有项目都有一个相同的依赖包,那么, 你在硬盘上就需要保存 100 份该相同依赖包的副本。然而,如果是使用 pnpm,依赖包将被 存放在一个统一的位置,因此:
- 如果你对同一依赖包需要使用不同的版本,则仅有 版本之间不同的文件会被存储起来。例如,如果某个依赖包包含 100 个文件,其发布了一个新 版本,并且新版本中只有一个文件有修改,则 pnpm update 只需要添加一个 新文件到存储中,而不会因为一个文件的修改而保存依赖包的 所有文件。
- 所有文件都保存在硬盘上的统一的位置。当安装软件包时, 其包含的所有文件都会硬链接自此位置,而不会占用 额外的硬盘空间。这让你可以在项目之间方便地共享相同版本的 依赖包。
最终结果就是以项目和依赖包的比例来看,你节省了大量的硬盘空间, 并且安装速度也大大提高了! 创建非扁平的node_modules目录
当使用 npm 或 Yarn Classic 安装依赖包时,所有软件包都将被提升到 node_modules 的 根目录下。其结果是,源码可以访问 本不属于当前项目所设定的依赖包。 默认情况下,pnpm 则是通过使用符号链接的方式仅将项目的直接依赖项添加到 node_modules 的根目录下。
从图上我们可以看出,pnpm平均比npm和yarn快上2~3倍。这一点在依赖的下载上额外明显。 为什么说pnpm会比npm和yarn更高效的利用磁盘空间? pnpm 有一个store的概念(是一块存储文件的空间,后面会说到),内部使用"基于内容寻址"的文件系统来存储磁盘上所有的文件,这一套系统的优点是:
你4个项目都依赖了express.js(第三方插件)。如果是npm/yarn的话,express.js就会被安装4个在你的磁盘空间当中。从而出现下面这个情况↓
但是pnpm 得益于"基于内容寻址"的文件系统,使用pnpm下载的文件不论被多少项目所依赖,都只会在磁盘中写入一次。后面再有项目使用的时候会采用硬链接的方式去索引那一块磁盘空间。 所以,在同样被多个项目依赖的时候,pnpm对磁盘的占用如下↓
如果有一天你所依赖的版本提升了。假设从express@2.0升级到了express@3.0。而express@3.0比express@2.0多了20个文件。这个时候pnpm并不会删除express@2.0再去重新下载express@3.0。而是复用express@2.0原本的磁盘内容。再在express@2.0的基础上增加20个文件形成express@3.0。 在npm1和npm2的时候。依赖结构是这样的↓ 代码示例(一)
node_modules └─ 依赖A ├─ index.js ├─ package.json └─ node_modules └─ 依赖B ├─ index.js └─ package.json └─ 依赖C ├─ index.js ├─ package.json └─ node_modules └─ 依赖B ├─ index.js └─ package.json
- 依赖包会被重复安装
- 依赖层级太多问题
还是以 代码示例(一) 为例子。A依赖B,C也依赖B。也就是说B同时是A和C的依赖。这种情况下。B会被下载两次。npm1和npm2的运行逻辑是,某一个包被其他包依赖N次,就需要被下载N次。也就是我们所说的重复安装。
node_modules └─ 依赖A ├─ index.js ├─ package.json └─ node_modules └─ 依赖B ├─ index.js ├─ package.json └─ node_modules └─ 依赖C ├─ index.js ├─ package.json └─ node_modules └─ 依赖D ├─ index.js └─ package.json
上方的代码示例就有四层。如果我们有某一个依赖了10层呢?他也一样会一层一层依赖下去。像是"依赖地狱"。可读性不高。 npm3和和yarn中的平铺的结构依然存在的问题 上述npm1和npm2的这些问题在npm3+和yarn中得到了解决 从npm3开始。npm3和yarn都采用了"扁平化依赖"来解决上述问题。 采用了扁平化管理之后,代码示例(一) 就从嵌套结构变成了扁平化结构,像这样↓ 代码示例(二)
node_modules └─ 依赖A ├─ index.js ├─ package.json └─ node_modules └─ 依赖C ├─ index.js ├─ package.json └─ node_modules └─ 依赖B ├─ index.js ├─ package.json └─ node_modules
所有的依赖都会被平铺到同一层面。这样,因为require寻找包的机制。如果A和C都依赖了B。那么A和C在自己的node_modules中未找到依赖C的时候会向上寻找,并最终在与他们同级的node_modules中找到依赖包C。这样,就不会出现重复下载的情况。而且依赖层级嵌套也不会太深。因为没有重复的下载,所有的A和C都会寻找并依赖于同一个B包。 这种平铺的结构看似完美,但其实依然存在一些细节上的问题。
- 依赖结构的不确定性
- 扁平化算法的复杂度比较高,相对的比较耗时。
- 项目中还是存在可以非法访问的问题