vue3 的 2 种响应式实现、Vue2 项目如何升级到 Vue3 等等。。。

一、options api 选项式 API

Options Api 可以理解为就是组件的各个选项,data、methods、computed、watch 等等就像是组件的一个个选项,在对应的选项里做对应的事情。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export default {
data () {
return {
// 定义响应式数据的选项
}
},
methods: {
// 定义相关方法的选项
},
computed: {
// 计算属性的选项
},
watch: {
// 监听数据的选项
}
...
}

在 data 中定义的数据,是无法做到响应式的,因为 Object.definePropety 只会对 data 选项中的数据进行递归拦截

在实际项目的开发过程中,数据定义在 data 中,方法定义在 methods 中,当我们的代码多起来,比如达到四、五百行的时候,如果我们想改动某个功能,就要去 data 中改数据,再去 methods 中改方法,来回地寻找。

二、composition api 组合式 api

1、Composition Api

支持将相同的功能模块代码写在一起,甚至可以将某个功能单独的封装成函数,随意导入引用;也可以将任意的数据定义成响应式,再也不用局限于 data 中,我们只需要将每个实现的功能组合起来就可以了。

示例:

1
2
3
4
5
6
7
<template>
<div>{{count}}</div>
</template>
<script setup>
import { ref } from "vue";
let count = ref(0);
</script>

2、watchEffect

(1)、watchEffect 是立即执行的,不需要添加 immediate 属性。

(2)、watchEffect 不需要指定对某个具体的数据监听,watchEffect 会根据内容自动去感知,所以我们也可以在一个 watchEffect 中添加多个数据的监听处理

(3)、watchEffect 不能获取数据改变之前的值。

同时,watchEffect 会返回一个对象 watchEffectStop,通过执行 watchEffectStop,我们可以控制监听在什么时候结束。

简单理解 watchEffect 会在第一次运行时创建副作用函数并执行一次,如果存在响应式变量,取值会触发 get 函数,这个时候收集依赖存储起来,当其他地方给响应式变量重新赋值的时候,set 函数中会触发方法派发更新,执行收集到的副作用函数,如果不存在响应式变量,就不会被收集触发

1、watchEffect 立即运行一个函数,然后被动地追踪它的依赖,当这些依赖改变时重新执行该函数。watch 侦测一个或多个响应式数据源并在数据源变化时调用一个回调函数。
2、watchEffect(effect)是一种特殊 watch,传入的函数既是依赖收集的数据源,也是回调函数。如果我们不关心响应式数据变化前后的值,只是想拿这些数据做些事情,那么 watchEffect 就是我们需要的。watch 更底层,可以接收多种数据源,包括用于依赖收集的 getter 函数,因此它完全可以实现 watchEffect 的功能,同时由于可以指定 getter 函数,依赖可以控制的更精确,还能获取数据变化前后的值,因此如果需要这些时我们会使用 watch。
3、watchEffect 在使用时,传入的函数会立刻执行一次。watch 默认情况下并不会执行回调函数,除非我们手动设置 immediate 选项。
4、watchEffect(fn)相当于 watch(fn,fn,{immediate:true})

vue3 不再只能有一个根元素

为什么在 vue2 时,只能拥有一个根元素?而 Vue3 可以写多个根节点?

因为 vdom 是一颗单根树形结构,patch 方法在遍历的时候从根节点开始遍历,它要求只有一个根节点。组件也会转换为一个 vdom,自然应该满足这个要求。vue3 中之所以可以写多个根节点,是因为引入了 Fragment 的概念,这是一个抽象的节点,如果发现组件是多根的,就创建一个 Fragment 节点,把多个根节点作为它的 children。将来 patch 的时候,如果发现是一个 Fragment 节点,则直接遍历 children 创建或更新。

3、ref 和 reactive

ref 和 reactive 的区别是什么呢,我们可以这样简单理解,它们都是用来定义响应式数据的,但是 ref 是用来给简单的数据类型定义响应式数据的,比如 number、string、boolean 等,而 reactive 是针对复杂的数据结构的,比如一个对象。

它们写法的区别主要在:ref 定义的数据,修改的时候是需改 xxx.value 的,而 reactive 定义的不用,产生这个区别的原因是它们实现响应式的方法不一样。

4、小结

Options Api

1、选项式的 api,相关代码必须写在规定的选项中,导致相同功能的代码被分割,代码量上来后查找相关代码很麻烦,后期维护修改难度较大。

2、数据都挂载在同一个 this 下,对 typescript 的支持不友好,类型推断很麻烦。

3、代码的复用能力很差。

Composition Api

1、组合式 api,代码定义很自由,相同功能代码整合到一起,查找修改都很方便。

2、公共代码的复用很简单,不同功能的代码也可以自由组合。

3、Vue 相关的 api 都是通过 import 导入的,这在打包的时候很友好。

另外,vue3 是支持 options api 的写法的

三、Vue3 响应式的实现

在 Vue2.x 中,响应式的机制深入人心,我们只需要在 data 中定义我们需要的数据,就会在初始化时被自动转为响应式数据。

但是在 Vue2 中,响应式的使用还存在一些限制,比如对象属性的增加和删除等并不能被监听到,在 Vue3 中,重新设计了响应式系统来解决这些问题。

1、Vue2.x 的响应式——Object.defineProperty

Vue2 响应式失效的现象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<template>
<div>
<span>姓名:</span>
<span>{{person.name}}</span>

<span>年龄:</span>
<span>{{person.age}}</span>

<button @click="changeName">修改姓名</button>
<button @click="addAge">增加年龄</button>
</div>
</template>
export default {
data () {
return {
person: {
name: '小明'
}
}
},
methods: {
addAge() {
this.person.age = '18';
},
changeName() {
this.person.name = '小红';
}
}
...
}

data 里面只定义了一个响应式对象 person,我们定义了 2 个方法,一个是修改名称、一个是增加年龄,但是使用增加年龄方法时,会给响应式对象添加一个新的属性 age,页面上的年龄部分并不会发生改变。

Vue2 是通过 Object.defineProperty 循环遍历拦截 data 中的数据来实现响应式的。

Object.defineProperty 其实不是真正的代理,而应该是拦截

而且 Object.defineProperty 也不是对对象进行拦截,而是拦截对象的具体的某个属性。

1
2
3
4
5
6
7
8
9
10
11
12
const person = {}

Object.defineProperty(person, 'name', {
set(value) {
console.log('name:', value)
},
get() {
return '小明'
}
})

console.log(person.name)

Vue2.x 的响应式实现其实就是递归遍历 data 中返回的对象,对每一个属性都使用 Object.defineProperty 进行拦截,而不在 data 中被初始化的数据是没有添加拦截的。

Vue2 如何添加和删除响应式数据?

需要额外的 api 来实现,Vue.$set 和Vue.$delete 方法分别实现添加、删除响应式数据

Vue2 响应式的局限性

1、无法监听整个对象,只能对每个属性单独监听。

2、无法监听对象的属性的新增,删除(需要补充额外的 api 来解决)。

3、无法监听数组的变化。

2、Vue3 的响应式-proxy

proxy 是真正地对整个对象进行代理,因为 proxy 可以劫持整个对象,所以 Object.defineProperty 中新增,删除某个属性无法检测的问题就不存在了,同时 proxy 也可以检测数组的变化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const person = {
name: '小明',
age: 18
}
const personProxy = new Proxy(person, {
get: function (target, prop) {
console.log(`获取了${prop}:`, target[prop])
return target[prop]
},
set: function (target, prop, value) {
console.log(`修改了${prop}:`, value)
target[prop] = value
}
})

console.log('name:', personProxy.name) // 获取了name:小明
personProxy.age = 20 // 修改了age:20

参数 target,表示当前代理的对象,prop 是我们具体要操作的属性,set 多了一个参数 value 是我们对新属性的赋值。

从方法的参数我们其实就能看出来,proxy 是真的对整个对象进行拦截的,我们如果有新增或删除的属性,也不需要单独去添加处理,可以直接被检测代理。

在添加删除属性时,无需额外的 api。proxy 不支持 IE11。

vue3 另外一个代理的方法,那就是对象本身的 get、set 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const count = {
_value: 0,
set value(num) {
console.log('修改了count:', num)
this._value = num
},
get value() {
console.log('获取了count')
return this._value
}
}

console.log(count.value) // 获取了count
count.value = 1 // 修改了count: 1

这其实就是为什么我们使用 ref 定义的数据,赋值和取值的时候需要使用 xxx.value 了

一个 Vue3 composition api 常用的工具集:VueUse

四、Vue2 升级到 Vue3 的非兼容性变更

Vue3 中做了很多重构,有部分内容对于 Vue2 来说是不兼容的,所以说 Vue2 的代码直接升级到 Vue3 是不能直接运行的。

1、createApp 的非兼容性变更

Vue2 根实例挂载及全局组件注册方法:

1
2
3
4
5
6
7
8
9
10
import Vue from 'vue'
import App from './App.vue'
// 引入全局组件
import GlobalComponent from './GlobalComponent.vue'
// 注册全局组件
Vue.component('GlobalComponent', GlobalComponent)

new Vue({
render: (h) => h(App)
}).$mount('#app')

Vue3 不直接在 Vue 对象上进行操作了,而是通过 createApp 来创建一个 App 应用实例,所有的操作都在 App 上进行

1
2
3
4
import { createApp } from 'vue'
import App from './App.vue'

createApp(App).mount('#app')

现在我们想要在一个 App 上引入 store,就可以使用下面的写法(全局对象被共享是一件非常危险的事情)

1
2
3
4
5
// 引入封装好的store
import store from './store'

createApp(App).use(store).mount('#app')
createApp(App2).mount('#app2')

App 实例上的 store 不会影响 App2

2、api 的 import 导入

我们在使用这些挂载在 Vue 对象下的 Api 时,需要经过 import 导入的方式来使用。

1
2
3
4
5
import { nextTick } from 'vue';

nextTick(() => {
...
})

按需加载的使用。

在 Vue2 的 Api 中,都是挂载在 Vue 下面,那么在打包的时候,会不管你有没有使用到这个 Api,都会一起打包进去,如果都是这样,随着 Vue 的全局 Api 越来越多,冗余的代码也就越多,打包的耗时、体积或者说代价也就越大。

在 Vue3 中,通过 import 导入 Api 来使用,那我们在打包的时候,则只会将对应的模块打包进去,做到真正的用了多少就打包多少,就算 Vue 中再增加多少代码,也不会影响我们打包的项目。

3、小结

升级 Vue3 不仅需要更换 Vue 版本,还有一些非兼容性变更内容需要了解

  1. 全局的操作不再使用 Vue 实例,而是使用通过 createApp 创建的 app 实例。
  2. 全局和内部 API 已经被重构,需要使用 import 导入使用,并且支持 tree-shake。

如何将 localStorage 中的数据变成响应式的?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// tool.js
import { ref, watchEffect } from 'vue'

const useLocalStorage = (name, value = {}) => {
const localData = ref(JSON.parse(localStorage.getItem(name)) || value)

watchEffect(() => {
// 监听本地localstorage数据对应的响应式变量更改
localStorage.setItem(name, JSON.stringify(localData.value))
})

return localData
}

export { useLocalStorage }

假设有一个计数器,需要将数据同步到本地的 localStorage 中,我们只需要在计数器文件中引入 useLocalStorage 方法即可:

1
2
3
4
5
6
7
8
9
10
<script setup>
import {useLocalStorage} from './useLocalStorage';
// 定义响应式数据
let count = useLocalStorage('count', 0);

const addCount = () => {
count.value ++;
}

</script>

五、如何将 Vue2 项目升级到 Vue3?

1、项目升级方法一

将 vue-cli 升级到高版本

Vue 的依赖版本升级到 vue3,需要安装@vue/compat

注意@vue/compat 的版本号需要与 Vue 的版本号保持一致

@vue/compat 是 Vue2 和 Vue3 的一个过渡产物,@vue/compat 可以运行在 Vue2 的环境下,但会对 Vue3 不兼容或者废弃的部分进行警告,我们引入@vue/compat 后,只需要根据警告的内容进行修改就可以了。
通过@vue/compat 也可以对警告进行分类过滤,单独针对某一些问题进行修改.

安装完 Vue 和@vue/compat 的依赖后,还需要在项目根目录下新增 vue.config.js 文件,包含以下内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// vue.config.js
module.exports = {
chainWebpack: (config) => {
config.resolve.alias.set('vue', '@vue/compat')

config.module
.rule('vue')
.use('vue-loader')
.tap((options) => {
return {
...options,
compilerOptions: {
compatConfig: {
MODE: 2
}
}
}
})
}
}

运行项目,根据警告信息的内容去 Vue 官网的特性参考中查询具体的错误原因,以及修改方案。

vue-router 和 vuex 都要升级到 v4,Element-ui 也要更新到 Vue3 对应的版本(element-plus)

vue3 版本 router 部分的写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import { createRouter, createWebHashHistory } from 'vue-router'

import Home from '../pages/home.vue'
import Login from '../pages/login.vue'

const routes = [
{
path: '/home',
component: Home
},
{
path: '/',
component: Login
}
]

export default createRouter({
history: createWebHashHistory(),
routes
})
1
2
3
4
5
6
7
8
// main.js
import { createApp } from 'vue'

import App from './App.vue'
import routers from './router'

const app = createApp(App)
app.use(routers).mount('#app')

2、项目升级方法二:gogocode

1、安装最新的 gogocode-cli

1
npm install gogocode-cli -g

2、在需要升级的项目根目录下,运行下面的指令

1
gogocode -s ./src -t gogocode-plugin-vue -o ./src-out

-s 后面指的是需要升级的源码文件夹,-o 后面的参数指的是升级后的代码输出位置

3、代码转换了还不够,我们项目的依赖都要升级到对应版本
GOGOCode 可以帮我们把 package.json 里面的 Vue/Vuex/Vue-router/Vue 编译工具升级到适配 Vue3 的版本,在项目根目录下执行以下指令

1
gogocode -s package.json -t gogocode-plugin-vue -o package.json

虽然使用了 GOGOCode,但也不代表我们的项目就可以直接完成升级,项目中如果用到了其他 Vue2 版本的组件库,还是需要我们自己去升级 Vue3 对应的版本,包括一些 Api 的变化都要我们自己去手动调整,并且使用 GOGOCode 也有一些转化规则是不支持的,具体的可以参考 GOGOCode 的转化规则覆盖。

六、vite

1、什么是 ES Module?

将 JavaScript 程序拆分为可按需导入的单独模块的机制,简单来说就是我们可以对 JavaScript 模块化开发,通过 import 和 export 来导入导出我们的模块内容

Vite 在冷启动的时候,将代码分为依赖和源码两部分,源码部分通常会使用 ESModules 或者 CommonJS 拆分到大量小模块中,而对于依赖部分,Vite 使用 Esbuild 对依赖进行预构建

Esbuild 的优势:

1、语言优势,Esbuild 使用 Go 语言开发,相对于 JavaScript,Go 语言是一种编译型语言,在编译阶段就已经将源码转译为机器码。

2、多线程,Rollup 和 webpack 都没有使用多线程的能力,而 Esbuild 在算法上进行了大量的优化,充分的利用了多 CPU 的优势。

2、搭建 vite 项目

1
2
// node 16.3.0
npm init vite@latest

3、vite 项目的目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
    |-- .gitignore
|-- index.html
|-- package-lock.json
|-- package.json
|-- README.md
|-- vite.config.js
|-- .vscode
| |-- extensions.json
|-- public
| |-- favicon.ico
|-- src
|-- App.vue
|-- main.js
|-- assets
| |-- logo.png
|-- components
|-- HelloWorld.vue

// src/
|-- src
|-- App.vue
|-- main.js
|-- api -- 请求数据,接口文件
|-- assets -- 静态资源
|-- commons -- 公共文件(公共方法,封装函数)
|-- components -- Vue组件
|-- pages -- 模块页面
|-- router -- 路由文件
|-- store -- 数据管理

4、安装 vue-router 的 4.x 版本

1
npm install vue-router@4

在 router 文件下新建 router.js 文件来增加路由控制,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import { createRouter, createWebHashHistory } from 'vue-router'

import Login from '../pages/login.vue'
import Home from '../pages/home.vue'

const routes = [
{
path: '/login',
component: Login
},
{
path: '/home',
component: Home
}
]

const router = createRouter({
history: createWebHashHistory(),
routes
})

export default router

createRouter 用来创建一个可以被 Vue 应用程序使用的路由实例,需要传入两个参数,history 是表示路由的历史记录,我们可以选择使用 createWebHistory、createWebHashHistory 来分别创建 HTML5 历史记录和 hash 历史记录,我们这里选择创建 hash 历史记录

七、输入路由的时候是怎么获取到页面的

当我们在浏览器中输入一个地址后,浏览器会根据路径构建一个请求,接下来就会对输入的域名进行 DNS 解析,得到正确的 IP 地址,然后和得到的 IP 地址建立 TCP 链接,发送 HTTP 请求,服务器接收到请求后,就会返回响应的 HTML 内容。完成了请求和响应后,浏览器拿到了返回的 HTML 字符串,转换成 DOM 树结构,经过对 DOM 的样式计算,最终生成布局,在页面上进行合成渲染

八、性能优化

1、路由懒加载:有效拆分 APP 尺寸,访问时才异步加载

(1)当打包构建应用时,JavaScript 包会变得非常大,影响页面加载。利用路由懒加载我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样会更加高效,是一种优化手段。

(2)一般来说,对所有的路由都使用动态导入是个好主意。

(3)给 component 选项配置一个返回 Promise 组件的函数就可以定义懒加载路由。例如:
{ path: ‘/users/:id’, component: () => import(‘./views/UserDetails’) }

(4)结合注释() => import(/_ webpackChunkName: “group-user” _/ ‘./UserDetails.vue’)可以做 webpack 代码分块
vite 中结合 rollupOptions 定义分块

(5)路由中不能使用异步组件

2、keep-alive 缓存页面:避免和重复创建组件实例,且能保留缓存组件状态

3、v-show 复用 DOM:避免重复创建组件

4、v-once 和 v-memo:不再变化的数据使用 v-once;按条件跳过更新时使用 v-memo

5、长列表性能优化:如果是大数据长列表,可采用虚拟滚动,只渲染少部分区域的内容

6、事件的销毁:Vue 组件销毁时,会自动解绑它的全部指令及事件监听器,但是仅限于组件本身的事件

7、图片懒加载:图片过多的页面,为了加速页面加载速度,所以很多时候我们需要将页面内未出现在可视区域内的图片先不做加载, 等到滚动到可视区域后再去加载。可以使用 vue-lazyload

8、第三方插件按需引入:例如组件库(element-plus)

9、服务端渲染:首屏渲染慢,可以考虑 SSR \ SSG