一、环境搭建
1. Node官网安装Node,推荐下载长期支持版。
检查是否安装成功Node,在cmd输入 node -v。若出现版本号就是安装成功。
安装完node,会附带npm命令。输入npm -v 就能查看npm的版本。
若需要cnpm命令,安装淘宝镜像 npm install -g cnpm --registry=https://registry.npm.taobao.org
安装完成后用cnpm -v查看安装的版本号。
windows若出现问题——'cnpm' 不是内部或外部命令,也不是可运行的程序或批处理文件。可以根据cmd上显示的cnpm安装的地址,找到cnpm文件夹拷贝到npm文件夹所在的目录。
或 npm config set prefix "npm所在的位置"
2. vue环境
1.安装vue和vue-cli脚手架
//最新稳定版(安装和更新的代码相同)
npm install vue@next
npm install -g @vue/cli
//或一起安装
npm install -g vue @vue/cli
保证vue/cli版本在4.5.0以上,才能更好的支持3.0。
用vue -V查看安装的版本号检验是都成功安装。
2. 创建vue项目
npm create <project-name>
如果想构建一个webpack的vue项目
npm init webpack <project-name>
也可以使用 Vite 快速构建 Vue 项目(Vite是一个 web 开发构建工具,由于其原生 ES 模块导入方式,可以实现闪电般的冷服务器启动)
npm init vite <project-name> -- --template vue
安装事项:这里会询问使用哪个模板进行安装,我们选择自定义安装的方法Manually select features
根据需要选择功能
选择vue版本及其他
安装完成,系统默认npm run serve运行
二、特性
Vue3.0与Vue2.x比,
1、性能方面有很大的提升——打包大小减少41%、初次渲染快55%,更新快133%、内存使用减少54%(得益于虚拟DOM的重写和Tree-shaking的优化)。
2、使用Composition API。Vue2.x使用组件选项来组织逻辑,随着复杂度上升易读性比较差,Vue3.0使用Composition API按照逻辑分类,代码维护和易读性较好。虽然Vue2.x给出Mixin来解决这个问题,但是Mixin会存在命名冲突的问题,不清楚暴露出来变量的最用及重用逻辑到其他Component经常会遇到的问题。
3、对TypeScript的支持更好
Tree-shaking
Vue3最重要的变化之一就是引入了Tree-Shaking,Tree-Shaking带来的bundle体积更小是显而易见的。在2.x版本中,很多函数都挂载在全局Vue对象上,比如nextTick、mixin、set等函数,因此虽然我们可能用不到,但打包时只要引入了vue这些全局函数仍然会打包进bundle中。
而在Vue3中,所有的API都通过ES6模块化的方式引入,这样就能让webpack或rollup等打包工具在打包时对没有用到API进行剔除,最小化bundle体积;
简而言之,Tree-Shaking这个打包工具在打包的时候不会把没用到的API打包进去。
//dog.vue
export function dogName(){
return 'dogName'
}
export function dogAge(){
return 'dogAge'
}
//index.vue 没使用dogAge所以dogAge不会被打包进去
import { dogName,dogAge} from './dog'
dogName()
全局API的修改:main.js中能直接发现这样的变化:
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
createApp(App).use(store).use(router).mount('#app')
创建app实例方式从原来的new Vue()变为通过createApp函数进行创建;不过一些核心的功能比如virtualDOM更新算法和响应式系统无论如何都是会被打包的;这样带来的变化就是以前在全局配置的组件(Vue.component)、指令(Vue.directive)、混入(Vue.mixin)和插件(Vue.use)等变为直接挂载在实例上的方法;我们通过创建的实例来调用,带来的好处就是一个应用可以有多个Vue实例,不同实例之间的配置也不会相互影响,且可以完美的支持tree-shaking:
const app = createApp(App)
app.use(/* ... */)
app.mixin(/* ... */)
app.component(/* ... */)
app.directive(/* ... */)
Vue 2.x 中的这些全局 API 受此更改的影响:
setup
先说说 选项式API和组合式 API。
选项式API
2.x版本采用的是Options API(选项API)。在 props 里面设置接收参数,在 data 里面设置变量,在 computed 里面设置计算属性,在 watch 里面设置监听属性,在 methods 里面设置事件方法
使用 (data、computed、methods、watch) 组件选项来组织逻辑通常都很有效。然而,当我们的组件开始变得更大时,逻辑关注点的列表也会增长。尤其对于那些一开始没有编写这些组件的人来说,这会导致组件难以阅读和理解。
这种碎片化使得理解和维护复杂组件变得困难。选项的分离掩盖了潜在的逻辑问题。此外,在处理单个逻辑关注点时,我们必须不断地“跳转”相关代码的选项块。
如果能够将同一个逻辑关注点相关代码收集在一起会更好。而这正是组合式 API 使我们能够做到的。
组合式 API
而Composition API做的就是把同一功能的代码放到一起维护,这样我们需要维护一个功能点的时候,不用去关心其他的逻辑,只关注当前的功能。
实际使用组合式 API 的地方,在 Vue 组件中,我们将此位置称为 setup。也就是将(生命周期、computed、watch、provide/inject)写在 setup 里。
注意:
因为 setup
是围绕 beforeCreate 和 created 生命周期钩子运行的,所以在生命周期钩子中编写的任何代码都应该直接在 setup 函数中编写。
没有this。因为setup()
是在解析其它组件选项之前被调用的,所以 setup() 内部的 this 的行为与其它选项中的 this 完全不同。
setup
选项是一个接收 props
和 context
的函数,此外,我们将 setup
返回的所有内容都暴露给组件的其余部分 (计算属性、方法、生命周期钩子等等) 以及组件的模板。
export default {
name: "Button",
setup() {
const state = reactive({
count: 1,
});
const num = ref(2);
function add() {
state.count++;
num.value += 10;
}
const double = computed(() => state.count * 2);
return {
state,
double,
num,
add,
};
},
};
生命周期
在Vue2.x中有8个生命周期函数:
beforeCreate、created、beforeMount、mounted、beforeUpdate、updated、beforeDestroy、destroyed
在vue3中,新增了一个setup生命周期函数,setup执行的时机是在beforeCreate生命函数之前执行,因此在这个函数中是不能通过this来获取实例的;同时为了命名的统一,将beforeDestroy改名为beforeUnmount,destroyed改名为unmounted。增加了onRenderTracked,onRenderTriggered两个钩子函数作追踪调试功能。
同时,我们可以通过在生命周期函数前加on来访问组件的生命周期。
三、新增功能
响应式API
要为 JavaScript 对象创建响应式状态,可以使用 reactive 方法:
import { reactive } from 'vue'
// 响应式状态
const state = reactive({
count: 0
})
reactive 相当于 Vue 2.x 中的 Vue.observable() API
reactive函数只接收object和array等复杂数据类型。
对于一些基本数据类型,比如字符串和数值等,我们想要让它变成响应式,我们当然也可以通过reactive函数创建对象的方式,但是Vue3提供了另一个函数ref:
import { ref } from 'vue'
const count = ref(0)
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
ref 会返回一个可变的响应式对象,该对象作为一个响应式的引用维护着它内部的值,这就是 ref 名称的来源。该对象只包含一个名为 value 的 property。
当 ref 作为渲染上下文 (从 setup() 中返回的对象) 上的 property 返回并可以在模板中被访问时,它将自动浅层次
解包内部值。只有访问嵌套的 ref 时需要在模板中添加 .value
:
<template>
<div>
<span>{{ count }}</span>
<button @click="count ++">Increment count</button>
<button @click="nested.count.value ++">Nested Increment count</button>
</div>
</template>
<script>
import { ref } from 'vue'
export default {
setup() {
const count = ref(0)
return {
count,
nested: {
count
}
}
}
}
</script>
注意:reactive主要负责复杂数据结构,而ref主要处理基本数据结构;但是很多童鞋就会误解ref只能处理基本数据,ref本身也是能处理对象和数组的。
区别:在语法层面,两个有差异。ref定义的响应式数据需要用[data].value的方式进行更改数据;reactive定义的数据需要[data].[prpoerty]的方式更改数据。
但是在应用的层面,还是有差异的,通常来说:单个的普通类型的数据,我们使用ref来定义响应式。表单场景中,描述一个表单的key:value这种对象的场景,使用reactive;在一些场景下,某一个模块的一组数据,通常也使用reactive的方式,定义数据。
访问响应式对象
vue2.0中,通过vue.set(object, propertyName, value) 方法向嵌套对象添加响应式 property。因为Vue 无法检测 property 的添加或移除。由于 Vue 会在初始化实例时对 property 执行 getter/setter 转化,所以 property 必须在 data 对象上存在才能让 Vue 将它转换为响应式的。
vue3.0,当 ref 作为响应式对象的 property 被访问或更改时,为使其行为类似于普通 property,它会自动解包内部值:
const count = ref(0)
const state = reactive({
count
})
console.log(state.count) // 0
state.count = 1
console.log(count.value) // 1
如果将新的 ref 赋值给现有 ref 的 property,将会替换旧的 ref:
const otherCount = ref(2)
state.count = otherCount
console.log(state.count) // 2
console.log(count.value) // 1
当我们处理一些大型响应式对象的property时,我们很希望使用ES6的解构来获取我们想要的值:
let book = reactive({
name: 'Learn Vue',
year: 2020,
title: 'Chapter one'
})
let {
name,
} = book
name = 'new Learn'
console.log(book.name);// Learn Vue
遗憾的是,使用的 property 的响应性丢失。对于这种情况,我们需要将我们的响应式对象转换为一组 ref。这些 ref 将保留与源对象的响应式关联:
let book = reactive({
name: 'Learn Vue',
year: 2020,
title: 'Chapter one'
})
let {
name,
} = toRefs(book)
// 注意这里解构出来的name是ref对象
// 需要通过value来取值赋值
name.value = 'new Learn'
console.log(book.name);// new Learn
使用 readonly 防止更改响应式对象
let book = reactive({
name: 'Learn Vue',
year: 2020,
title: 'Chapter one'
})
const copy = readonly(book);
//Set operation on key "name" failed: target is readonly.
copy.name = "new copy";
响应式计算和侦听
计算值
有时我们需要的值依赖于其他值的状态。在vue2.x中我们使用computed函数来进行计算属性,在vue3中将computed功能进行了抽离,它接受一个getter函数,并为getter返回的值创建了一个不可变的
响应式ref对象:
const num = ref(0);
const double = computed(() => num.value * 2);
num.value++;
console.log(double.value);// 2
double.value = 4// Warning: computed value is readonly
或者,它可以使用一个带有 get 和 set 函数的对象来创建一个可写
的 ref 对象。
const num = ref(0);
const double = computed({
get: () => num.value * 2,
set: (val) => (num.value = val / 2),
});
num.value++;
console.log(double.value);// 2
double.value = 8
console.log(num.value);// 4
侦听
和computed相对应的就是watch,computed是多对一的关系,而watch则是一对多的关系;vue3也提供了两个函数来侦听数据源的变化:watch和watchEffect。
watch,它的用法和组件的watch选项用法完全相同,它需要监听某个数据源,然后执行具体的回调函数:
import { reactive, ref, watch } from "vue";
const state = reactive({
count: 0,
});
//侦听时返回值得getter函数
watch(
() => state.count,
(count, prevCount) => {
console.log(count, prevCount); // 1 0
}
);
state.count++;
const count = ref(0);
//直接侦听ref
watch(count, (count, prevCount) => {
console.log(count, prevCount, "watch");// 2 0
});
count.value = 2;
如果我们来侦听一个深度嵌套的对象属性变化时,需要设置deep:true。
一般侦听都会在组件销毁时自动停止,但是有时候我们想在组件销毁前手动的方式进行停止,可以调用watch返回的stop函数进行停止。
const count = ref(0);
const stop = watch(count, (count, prevCount) => {
console.log(count, prevCount);// 打印1 0,2 1 后不执行
});
setInterval(()=>{
count.value++;
}, 1000);
// 停止watch
setTimeout(()=>{
stop();
}, 2500);
还有一个函数watchEffect也可以用来进行侦听,但是都已经有watch了,这个watchEffect和watch有什么区别呢?他们的用法主要有以下几点不同:
1、watchEffect不需要手动传入依赖
2、每次初始化时watchEffect都会执行一次回调函数来自动获取依赖
3、watchEffect无法获取到原值,只能得到变化后的值
const count = ref(0);
const state = reactive({
year: 2021,
});
watchEffect(() => {
console.log(count.value);
console.log(state.year);//0 2021,1,2022...
});
watch([() => state.year, count], (newVal, oldVal) => {
//[2022, 1] [2021, 0]
//[2023, 2] [2022, 1]...
console.log(newVal, oldVal);
});
setInterval(() => {
count.value++;
state.year++;
}, 1000);
Fragment
所谓的Fragment,就是片段;在vue2.x中,要求每个模板必须有一个根节点,所以我们代码要这样写:
<template>
<div>
<span></span>
<span></span>
</div>
</template>
或者在Vue2.x中还可以引入vue-fragments库,用一个虚拟的fragment代替div;在React中,解决方法是通过的一个React.Fragment标签创建一个虚拟元素;在Vue3中我们可以直接不需要根节点:
<template>
<span>hello</span>
<span>world</span>
</template>
Teleport
可以将插槽中的元素或者组件传送到页面的其他位置。就是在一些嵌套比较深的组件来转移模态框的位置。虽然在逻辑上模态框是属于该组件的,但是在样式和DOM结构上,嵌套层级后较深后不利于进行维护(z-index等问题);因此我们需要将其进行剥离出来:
<template>
<button @click="showDialog = true">打开模态框</button>
<teleport to="#modal">
//to可以定义最后组件在哪里显示
<div class="modal" v-if="showDialog" style="position: fixed">
我是一个模态框
<button @click="showDialog = false">关闭</button>
<child-component :msg="msg"></child-component>
</div>
</teleport>
</template>
<script>
export default {
data() {
return {
showDialog: false,
msg: "hello"
};
},
};
</script>
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title></title>
</head>
<body>
<noscript>
</noscript>
<div id="app"></div>
<div id="modal"></div>//teleport组件将加载在这里
<!-- built files will be auto injected -->
</body>
</html>
Suspense
Suspense是Vue3推出的一个内置组件,它允许我们的程序在等待异步组件时渲染一些后备的内容,可以让我们创建一个平滑的用户体验;Vue中加载异步组件其实在Vue2.x中已经有了,我们用的vue-router中加载的路由组件其实也是一个异步组件:
export default {
name: "Home",
components: {
AsyncButton: () => import("../components/AsyncButton"),
},
}
在Vue3中重新定义,异步组件需要通过defineAsyncComponent来进行显示的定义:
// 全局定义异步组件
//src/main.js
import { defineAsyncComponent } from "vue";
const AsyncButton = defineAsyncComponent(() =>
import("./components/AsyncButton.vue")
);
app.component("AsyncButton", AsyncButton);
// 组件内定义异步组件
// src/views/Home.vue
import { defineAsyncComponent } from "vue";
export default {
components: {
AsyncButton: defineAsyncComponent(() =>
import("../components/AsyncButton")
),
},
};
同时对异步组件的可以进行更精细的管理:
export default {
components: {
AsyncButton: defineAsyncComponent({
delay: 100,
timeout: 3000,
loader: () => import("../components/AsyncButton"),
errorComponent: ErrorComponent,
onError(error, retry, fail, attempts) {
if (attempts <= 3) {
retry();
} else {
fail();
}
},
}),
},
};
这样我们对异步组件加载情况就能掌控,在加载失败也能重新加载或者展示异常的状态。
四、非兼容的功能
非兼容的功能主要是一些和Vue2.x版本改动较大的语法,在Vue3上可能存在兼容问题。
v-model
非兼容:用于自定义组件时,v-model prop 和事件默认名称已更改:
prop:value -> modelValue;
event:input -> update:modelValue;
非兼容:v-bind 的 .sync 修饰符和组件的 model 选项已移除,可用 v-model 作为代替;
新增:现在可以在同一个组件上使用多个 v-model 进行双向绑定;
新增:现在可以自定义 v-model 修饰符。
<ChildComponent v-model:title="pageTitle" />
<!-- 是以下的简写: -->
<ChildComponent :title="pageTitle" @update:title="pageTitle = $event" />
v-if 与 v-for 的优先级对比
2.x 版本中在一个元素上同时使用 v-if 和 v-for 时,v-for 会优先作用
3.x 版本中 v-if 总是优先于 v-for 生效。
v-bind合并
在 2.x,如果一个元素同时定义了 v-bind="object" 和一个相同的单独的 property,那么这个单独的 property 总是会覆盖 object 中的绑定。
<!-- template -->
<div id="red" v-bind="{ id: 'blue' }"></div>
<!-- result -->
<div id="red"></div>
在 3.x,如果一个元素同时定义了 v-bind="object" 和一个相同的单独的 property,那么声明绑定的顺序决定了它们如何合并。换句话说,相对于假设开发者总是希望单独的 property 覆盖 object 中定义的内容,现在开发者对自己所希望的合并行为有了更好的控制。
<!-- template -->
<div id="red" v-bind="{ id: 'blue' }"></div>
<!-- result -->
<div id="blue"></div>
<!-- template -->
<div v-bind="{ id: 'blue' }" id="red"></div>
<!-- result -->
<div id="red"></div>
v-for中ref
在 Vue 2 中,在 v-for 中使用的 ref attribute 会用 ref 数组填充相应的 $refs property。当存在嵌套的 v-for 时,这种行为会变得不明确且效率低下。
<template>
<div v-for="item in list" :ref="setItemRef"></div>
</template>
<script>
export default {
data(){
list: [1, 2]
},
mounted () {
console.log(this.$refs.setItemRef) // [div, div]
}
}
</script>
在 Vue 3 中,此类用法将不再自动创建 $ref 数组。要从单个绑定获取多个 ref,请将 ref 绑定到一个更灵活的函数上 (这是一个新特性):
<template>
<div v-for="item in 3" :ref="setItemRef"></div>
</template>
<script>
import { reactive, onUpdated } from 'vue'
export default {
setup() {
let itemRefs = reactive([])
const setItemRef = el => {
itemRefs.push(el)
}
onUpdated(() => {
console.log(itemRefs)
})
return {
itemRefs,
setItemRef
}
}
}
</script>
Data
非兼容:组件选项 data 的声明不再接收纯 JavaScript object,而是接收一个 function。
非兼容:当合并来自 mixin 或 extend 的多个 data 返回值时,合并操作现在是浅层次的而非深层次的 (只合并根级属性)。
在 2.x 中,开发者可以通过 object 或者是 function 定义 data 选项。但是我们知道在组件中如果data是object的话会出现数据互相影响,因为object是引用数据类型。
在Vue3中,data只接受function类型,通过function返回对象;同时Mixin的合并行为也发生了改变,当mixin和基类中data合并时,会执行浅拷贝合并:
const Mixin = {
data() {
return {
user: {
name: 'Jack',
id: 1
}
}
}
}
const CompA = {
mixins: [Mixin],
data() {
return {
user: {
id: 2
}
}
}
}
//2.x
{
"user": {
"id": 2,
"name": "Jack"
}
}
//3.x
{
"user": {
"id": 2
}
}