vite 构建工具
vite 打包构建速度快于 webpack
搭建工程
1 | npm init vite-app 工程名称 |
安装依赖
1 | npm i |
启动项目
1 | npm run dev |
工程结构
*** ** 引用工程名需要加后缀.vue(cli 不用)
* 页面文件(index.html)放在了根目录里
vite 与 webpack 开发环境区别
webpack
考虑到浏览器兼容问题,给出一个入口文件,通过入口文件找依赖,找到很多模块,有 css、js 等模块然后 webpack 进行打包,通过loader、plugin等,打包后形成多个文件,然后启动开发服务器,通过打开地址访问开发服务器去访问打包后的结果。
在热更新方面,如果改动了一个模块后,那么模块所依赖的模块都需要重新打包编译,所以导致热更新也会很慢
vite
需要开发阶段浏览器兼容 ESModule,代码中不能使用 CommonJS
与 webpack 不同,vite 直接启动了开发服务器,所以速度很快。他和存在的文件大小无关,即使工程很大,因为不需要像 webpack 一样先打包,所以启动依旧很快,页面请求什么模块,就直接向开发服务器发送请求,服务器再对该模块进行实时编译,返回页面对应的模块内容(按需动态编译)
在热更新方面,当改动了一个模块后,仅需让浏览器重新请求该模块即可,开发服务器只对相应的模块进行实时编译返回,效率更高。
Composition api
vue2 使用的是 option api,意为选项式 api,不同配置在不同位置,在 data 里定义变量,在 methods 里写方法使用 data 里的数据,导致数据分散在各个位置,如果在复杂组件内,不易维护阅读。
vue3 使用 Composition api,意为组合式 api,使用这种写法,可以保证高内聚,数据、方法、计算属性等都定义在一个区域内,组件更易维护和阅读
基础写法
1 | <template> |
由于在 template 里是在实例环境里,可以直接使用 countRef 获取值;但是在 setup 中,countRef 是一个对象,实例代理中,countRef 是一个 countRef.value
将数据提取出去的写法
1 | <template> |
setup
1 | // component |
context 对象的成员
| 成员 | 类型 | 说明 |
|---|---|---|
| attrs | 对象 | 同vue2的this.$attrs |
| slots | 对象 | 同vue2的this.$slots |
| emit | 方法 | 同vue2的this.$emit |
生命周期函数
| vue2 option api | vue3 option api | vue 3 composition api |
|---|---|---|
| beforeCreate | beforeCreate | 不再需要,代码可直接置于 setup 中 |
| created | created | 不再需要,代码可直接置于 setup 中 |
| beforeMount | beforeMount | onBeforeMount |
| mounted | mounted | onMounted |
| beforeUpdate | beforeUpdate | onBeforeUpdate |
| updated | updated | onUpdated |
| beforeDestroy | ==改== beforeUnmount | onBeforeUnmount |
| destroyed | ==改==unmounted | onUnmounted |
| errorCaptured | errorCaptured | onErrorCaptured |
| - | ==新==renderTracked | onRenderTracked |
| - | ==新==renderTriggered | onRenderTriggered |
新增钩子函数说明:
| 钩子函数 | 参数 | 执行时机 |
|---|---|---|
| renderTracked | DebuggerEvent | 渲染 vdom 收集到的每一次依赖时 |
| renderTriggered | DebuggerEvent | 某个依赖变化导致组件重新渲染时 |
DebuggerEvent:
- target: 跟踪或触发渲染的对象
- key: 跟踪或触发渲染的属性
- type: 跟踪或触发渲染的方式
composition api 相比于 option api
composition api 相比于 option api 有哪些优势?
- 为了更好的逻辑复用和代码组织
- 更好的类型推导
- 有了 composition api,配合 reactivity api,可以在组件内部进行更加细粒度的控制,使得组件中不同的功能高度聚合,提升了代码的可维护性。对于不同组件的相同功能,也能够更好的复用。
- 相比于 option api,composition api 中没有了指向奇怪的 this,所有的 api 变得更加函数式,这有利于和类型推断系统比如 TS 深度配合。
Reactivity api
vue2 数据响应式没有暴露,直接将数据注入组件实例中(就是在 data 中定义变量等,这些被监听后变成了响应式数据)
vue3 将数据响应式暴露出来了,使用 setup 函数(composition api),导入的 unMounted,onUnmounted 和 setup 函数是属于 composition api,ref,computed,watchEffect 函数是属于 Reactivity api
处理的数据响应式,会运行一系列函数,如 watchEffect 和 render 函数等,这些是需要响应式数据支撑(数据变化需要被知道)
将数据变成响应式
| API | 传入 | 返回 | 备注 |
|---|---|---|---|
reactive |
plain-object |
对象代理 |
深度代理对象中的所有成员 |
readonly |
plain-object or proxy |
对象代理 |
只能读取代理对象中的成员,不可修改 |
ref |
any |
{ value: ... } |
对 value 的访问是响应式的 如果给 value 的值是一个对象, 则会通过 reactive函数进行代理如果已经是代理,则直接使用代理 |
computed |
function |
{ value: ... } |
当读取 value 值时, 会根据情况决定是否要运行函数 |
应用:
- 如果想要让一个对象变为响应式数据,可以使用
reactive或ref - 如果想要让一个对象的所有属性只读,使用
readonly - 如果想要让一个非对象数据变为响应式数据,使用
ref - 如果想要根据已知的响应式数据得到一个新的响应式数据,使用
computed
1 | import { reactivity, readonly } from "vue"; |
转换
unref
等同于:isRef(val) ? val.value : val
应用:
1 | function useNewTodo(todos) { |
toRef
得到一个响应式对象某个属性的 ref 格式
1 | const state = reactive({ |
toRefs
把一个响应式对象的所有属性转换为 ref 格式,然后包装到一个plain-object中返回
1 | const state = reactive({ |
应用:
1 | setup(){ |
使用 toRefs 原因
通过ref()或者 reactive()获得的响应式对象,如果对其进行解构,获得到的值不具有响应式属性,修改原属性后,解构后的值不会发生变化,此时就需要使用toRefs对需要解构的内容进行一次包裹,保证解构后的内容仍然具有响应式属性
降低心智负担,统一代码规范
为了减少上面的困难,最好有一个统一的规范,如果需要返回的内容是对象,就需要使用toRefs进行包裹,如下
所有的composition function均以ref的结果返回,以保证setup函数的返回结果中不包含reactive或readonly直接产生的数据
1 | function usePos(){ |
监听数据变化
watchEffect
1 | const stop = watchEffect(() => { |
watch
1 | // 等效于vue2的$watch |
注意:无论是watchEffect还是watch,当依赖项变化时,回调函数的运行都是异步的(微队列),即如果同时多次改变依赖数据,值返回一次回调函数内容
注意:如果要监控某个对象中的属性,不能直接监控 state.count,因为这个是一个值,需要监控一个函数()=>state.count
应用:除非遇到下面的场景,否则均建议选择watchEffect
- 不希望回调函数一开始就执行
- 数据改变时,需要参考旧值
- 需要监控一些回调函数中不会用到的数据(例如:监控依赖数据变化后对不是依赖的内容进行处理,这时候使用
watchEffect就监控不到了)
共享数据方案
Vuex
安装 vex
1 | npm i vuex@next |
两个重要变动:
- 去掉了构造函数
Vuex,而使用createStore创建仓库
1 | import { createStore } from "vuex"; |
- 为了配合
composition api,新增useStore函数获得仓库对象
1 | import { useStore } from "vuex"; |
global state
由于vue3的响应式系统本身可以脱离组件而存在,因此可以充分利用这一点,轻松制造多个全局响应式数据
1 | // store/useLoginUser 提供当前登录用户的共享数据 |
Provide&Inject
在vue2中,提供了provide和inject配置,可以让开发者在高层组件中注入数据,然后在后代组件中使用
除了兼容vue2的配置式注入,vue3在composition api中添加了provide和inject方法,可以在setup函数中注入和使用数据
考虑到有些数据需要在整个 vue 应用中使用,vue3还在应用实例中加入了provide方法,用于提供整个应用的共享数据
1 | creaetApp(App).provide("foo", ref(1)).provide("bar", ref(2)).mount("#app"); |
因此,我们可以利用这一点,在整个 vue 应用中提供共享数据
1 | // store/useLoginUser 提供当前登录用户的共享数据 |
Vue3 与 Vue2 区别
Vue3 没有 Vue 构造函数
即不存在
1 | import Vue from "vue"; |
新的创建方式,具名导出:
1 | import { createApp } from "vue"; |
vue3 需要使用createApp(App)创建应用
1 | // vue3 |
this 指向
vue2 的 this 指向组件实例
vue3 的 this 指向一个 proxy 代理,代理里面的内容类似于 vue2 实例,相当于在 vue2 的实例外多加了一层代理
v-model
vue2比较让人诟病的一点就是提供了两种双向绑定:v-model和.sync,在vue3中,去掉了.sync修饰符,只需要使用v-model进行双向绑定即可。
为了让v-model更好的针对多个属性进行双向绑定,vue3作出了以下修改
当对自定义组件使用
v-model指令时,绑定的属性名由原来的value变为modelValue,事件名由原来的input变为update:modelValue1
2
3
4
5
6
7
8
9
10
11
12<!-- vue2 -->
<ChildComponent :value="pageTitle" @input="pageTitle = $event" />
<!-- 简写为 -->
<ChildComponent v-model="pageTitle" />
<!-- vue3 -->
<ChildComponent
:modelValue="pageTitle"
@update:modelValue="pageTitle = $event"
/>
<!-- 简写为 -->
<ChildComponent v-model="pageTitle" />去掉了
.sync修饰符,它原本的功能由v-model的参数替代1
2
3
4
5
6
7
8
9<!-- vue2 -->
<ChildComponent :title="pageTitle" @update:title="pageTitle = $event" />
<!-- 简写为 -->
<ChildComponent :title.sync="pageTitle" />
<!-- vue3 -->
<ChildComponent :title="pageTitle" @update:title="pageTitle = $event" />
<!-- 简写为 -->
<ChildComponent v-model:title="pageTitle" />model配置被移除允许自定义
v-model修饰符vue2 无此功能
1
2
3
4<Comp v-model.cap="data1" v-model:text.cap="data2" />
// modelValue: data1 // modelModifiers: { cap: true} // text: data2 //
textModifiers: {cap: true}
v-if v-for
v-if 的优先级 现在高于 v-for
key
当使用
<template>进行v-for循环时,需要把key值放到<template>中,而不是它的子元素中当使用
v-if v-else-if v-else分支的时候,不再需要指定key值,因为vue3会自动给予每个分支一个唯一的key即便要手工给予
key值,也必须给予每个分支唯一的key,不能因为要重用分支而给予相同的 key
Fragment
vue3现在允许组件出现多个根节点
路由
需要先安装
1 | npm i vue-router@next |
在 router 文件夹下创建 index.js 文件
需要使用具名导出路由
1 | // index.js |
1 | // routes.js |
异步组件
情景:某个页面存在多个模块需要异步加载,根据自身加载情况展示不同情况的内容
vue2 需要配置一个动态的 import
在 vue3 中提供了一个函数处理异步组件,如下:
1 | import { defineAsyncComponent } from "vue"; // defineAsyncComponent函数需要返回一个promise |
注意:这里的 const 出的 Block3 要等到 import 加载完成才显示
除了以上的配置方法,还能配置成一个对象,有如下配置项:
1 | import { defineAsyncComponent, h } from "vue"; |
注意:在 vue2 中 render 函数中有一个参数 h,这个 h 是一个创建虚拟节点的 function,但是在 vue3 中,将这个 h 函数提升为一个全局普通函数了,可以在任何地方创建虚拟节点(vue3 中需要从 vue 中导出 h 函数)
异步页面
大致同上异步组件,只需要在路由中配置类似上面的 defineAsyncComponent,如下
1 | // util/index.js |
1 | // router/routes.js |
异步加载完成后,页面切换不会再重新获取资源,已经在浏览器缓存了
Teleport
当某个组件在逻辑组件结构上写在了深层次的结构里,但是他的物理真实 DOM 结构应该是在最外层,这时候可以使用 Teleport 将内容插到指定位置,比如一个蒙层组件,如下
1 | <div class="block"> |
以上的蒙层组件 Modal 写在了对应操作的元素标签附近,但是蒙层应该是属于 body 下的内容,这里可以使用 Teleport 的 to 属性指向 body 将该组件的 DOM 结构改到 body 下
Vue3 的性能优化
客户端渲染效率比 vue2 提升了 1.3~2 倍
SSR 渲染效率比 vue2 提升了 2~3 倍
静态提升
下面的静态节点会被提升
- 元素节点
- 没有绑定动态内容
1 | // vue2 的静态节点 |
静态属性会被提升,同上,减少了内存占用
1 | <div class="user">{{user.name}}</div> |
1 | const hoisted = { class: "user" }; |
预字符串化
1 | <div class="menu-bar-container"> |
当编译器遇到大量连续的静态内容,会直接将其编译为一个普通字符串节点
1 | const _hoisted_2 = _createStaticVNode( |
缓存事件处理函数
1 | <button @click="count++">plus</button> |
1 | // vue2 |
总结:将事件处理函数缓存起来,后续调用该处理函数时,判断是否存在缓存该处理函数,若存在缓存就不用再次重新注册创建这个处理函数,直接调用缓存即可
Block Tree
vue2 在对比新旧树的时候,并不知道哪些节点是静态的,哪些是动态的,因此只能一层一层比较,这就浪费了大部分时间在比对静态节点上
1 | <form> |
总结:vue3 会在 Block 根节点保存动态节点,对比时直接对动态节点进行比较,静态节点不需要做对比
PatchFlag
vue2 在对比每一个节点时,并不知道这个节点哪些相关信息会发生变化,因此只能将所有信息依次比对
1 | <div class="user" data-id="1" title="user name">{{user.name}}</div> |
总结:vue3 的 PatchFlag 将动态节点的动态属性标记,比如类样式是动态的,会将其标记为 1 #class,做对比时不需要对静态的信息部分进行对比,节省了对比次数
问题
使用 less 报错
问题描述:
npm run dev 后
报错信息如下
1 | [vite] Dep optimization failed with error: |
原因&解决:
需要使用
1 | npm i less -D |
将 less 装在 package.json 的开发依赖中(devDependencies)