vue3.0六大亮点
-
Performance:通过
Proxy实现双向响应式绑定,相比defineProperty的遍历属性的方式效率更高,性能更好,另外Virtual DOM更新只diff动态部分、事件缓存等,也带来了性能上的提升 -
Tree-Shaking Support:相比2.x导入整个Vue对象,3.x支持按需导入,只打包需要的代码
-
Composition API:组合式API,面向函数编程
-
Fragment、Teleport、Suspense:“碎片”,Teleport即Protal传送门,“悬念”,参考了React的设计
-
Better Typescript support:2.x设计之初没有考虑到类型推导,导致适配ts比较困难,3.x移除了this对象,利用了天然对类型友好的普通变量与函数,对TypeScript支持更好
-
Custom Render API:
目前vue3.0已经进入最终测试版Release Candidate阶段,相信正式版很快就可以和大家见面
RC阶段意味着API已经成熟稳定,这个阶段只是改改bug,没有大问题的话不会有功能变动。
vue3.0为了兼顾2.x的升级,将支持2.x的大部分语法,包括现在的data、methods、computed、watch、hooks等,但并不是全部兼容,另外还包含一些其他修改,需要手动修改支持,所以3.0并不是无缝对接2.x,只是兼容了大部分,减少升级成本。
快速搭建vue3.0项目
1、安装vue cli
npm install -g @vue/cli
之前已经安装过vue-cli的话,可以升级到最新版
npm update -g @vue/cli // 升级vue-cli到最新版
vue -V // 查看vue-cli版本
2、创建vue3.0项目
vue create vue3-test
选择预设配置,这里我们选择人工选择(Manually select features)
选择功能配置,根据自己需要选择,不熟悉TypeScript的同学可以不选TypeScript,Unit Testing也可根据需求选择
下一步,选择 3.x (Preview)
下面这些自己根据需求选择即可,这里不再介绍
只需2步,就把vue3.x项目搭建起来了,下面开始看看vue3.x怎么使用,2.x怎么升级。
vue3.0新特性
1、实例化
2.x使用构造函数new Vue(...)创建实例,3.x使用createApp函数创建实例;2.x所有属性方法和设置都绑定到全局Vue对象上,3.x改为绑定到vue实例下,收紧了scope;3.x移除了Vue.config.productionTip和Vue.config.keyCodes配置属性;
// vue 2.x
import Vue from 'vue'
import App from './App'
import router from './router'
import store from './store'
Vue.config.ignoredElements = [/^app-/]
Vue.use(/* ... */)
Vue.mixin(/* ... */)
Vue.component(/* ... */)
Vue.directive(/* ... */)
Vue.prototype.customProperty = () => {}
new Vue({
el: '#app',
router,
store,
render: h => h(App)
})
// vue 3.x
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
const app = createApp(App)
app.config.isCustomElement = tag => tag.startsWith('app-')
app.use(/* ... */)
app.mixin(/* ... */)
app.component(/* ... */)
app.directive(/* ... */)
app.config.globalProperties.customProperty = () => {}
app.use(router).use(store).mount('#app')
2、创建页面
在/src/views目录中新建Test.vue
// vue 2.x
<template>
<div class="page-wrapper">
<span>这是一个新页面</span>
</div>
</template>
<script>
export default {
name: 'Test',
data() {
return {
}
}
}
</script>
// vue 3.x
<template>
<div class="page-wrapper">
<span>这是一个新页面</span>
</div>
</template>
<script>
export default {
name: 'Test',
setup () {
return {}
}
}
</script>
在 /src/router/index.js中创建路由
// vue 2.x
import Vue from 'vue';
import VueRouter from 'vue-router';
// import Home from '../views/Home.vue';
// import About from '../views/About.vue';
// import Test from '../views/Test.vue';
Vue.use(VueRouter);
export default new VueRouter({
mode: 'hash',
routes: [
{
path: '/Home',
name: 'Home',
// component: Home,
component: () => import(/* webpackChunkName: "Home" */ '../views/Home.vue')
},
{
path: '/about',
name: 'About',
// component: About,
component: () => import(/* webpackChunkName: 'About' */ '../views/About.vue')
},
{
path: '/test',
name: 'Test',
// component: Test,
component: () => import(/* webpackChunkName: "test" */ '../views/Test.vue')
}
]
})
// vue 3.x
import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
},
{
path: '/test',
name: 'Test',
component: () => import(/* webpackChunkName: "test" */ '../views/Test.vue')
}
]
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
})
export default router
3、Composition API
vue2.x中,所有的数据都在data方法中定义返回,方法定义在methods下面,并通过this调用, vue3.x中,所有的代码逻辑将在setup方法中实现,包括data、watch、computed、methods、hooks,并且不再有this
vue3.x setup方法在组件生命周期内只执行一次,不会重复执行
相比vue2.x中基于OPTIONS配置的方式,vue3.x基于组合式API的方式语义没有2.x清晰,2.x中data、methods、computed、watch等都通过不同的scope区分开,看起来很清晰,3.x都放在setup方法中,对代码组织能力会有更高的要求。
vue2.x使用Composition API可以安装@vue/composition-api,使用基本跟Composition API一样,这里不再赘述
① 状态和事件绑定 reactive & ref
reactive 几乎等价于 2.x 中的 Vue.observable() API,只是为了避免与 RxJS 中的 observable 混淆而做了重命名
vue3.x的reactive和ref取代了vue2.x中的data数据定义
从下面的代码中可以看到,reactive处理的是对象的双向绑定,而ref则可以处理js基础类型的双向绑定,其实ref的实现原理也只是对基础类型进行对象化封装,把数据放在{ value: 基础值 }里,再添加一个ref标识属性用来区分。
// vue2.x
export default {
name: 'Test',
data () {
return {
count: 0,
num: 0
}
},
methods: {
addCount () {
this.count++
}
addNum() {
this.num++
}
}
}
// vue3.x
<template>
<div class="page-wrapper">
<div>
<span>count 点击次数: </span>
<span>{{ count }}</span>
<button @click="addCount">点击增加</button>
</div>
<div>
<span>num 点击次数: </span>
<span>{{ num }}</span>
<button @click="addNum">点击增加</button>
</div>
</div>
</template>
<script>
import { reactive, ref, toRefs } from 'vue'
export default {
name: 'Test',
setup () {
const state = reactive({
count: 0
})
const num = ref(0)
const addCount = function () {
state.count++
}
const addNum = function () {
num.value++
}
return {
// 这样展开后state property会失去响应式,因为是取值返回,不是引用
// ...state,
...toRefs(state),
num,
addCount,
addNum
}
}
}
</script>
注意: setup在模板中访问时,从ref返回的引用将自动解包,因此模板中使用不需要.value。在setup中访问必须需要.value
setup 还可以返回一个render函数:
import { h, ref, reactive } from 'vue'
export default {
setup() {
const count = ref(0)
const object = reactive({ foo: 'bar' })
return () => h('div', [
count.value,
object.foo
])
}
}
接收props 数据
第一个接收的是props数据:
export default {
props: {
name: String
},
setup(props) {
console.log(props.name)
}
}
props数据可以用 watch 方法来监听:
export default {
props: {
name: String
},
setup(props) {
watch(() => {
console.log(`name is: ` + props.name)
})
}
}
在开发过程中,props对象不可更改(如果用户代码尝试对其进行更改,则会发出警告)
- 第二个参数提供了一个上下文对象,该对象公开了先前在2.x API中使用this公开的属式:
// 2.0 中 this.$emit()
const MyComponent = {
setup(props, context) {
console.log(context)
context.attrs
context.slots
context.emit
context.ref
}
}
context中的对象使用方式和2.0中的保持一致:
attrs并且slots是内部组件实例上对应值的代理。这样可以确保即使在更新后它们也始终显示最新值,以便我们可以对它们进行结构解析而不必担心访问陈旧的引用:
const MyComponent = {
setup(props, { attrs }) {
// a function that may get called at a later stage
function onClick() {
console.log(attrs.foo) // guaranteed to be the latest reference
}
}
}
this用法
this里面没有setup()。由于setup()是在解析2.x选项之前调用的,因此this内部setup()(如果可用)的行为将与this其他2.x选项完全不同。避免this进入的另一个原因setup()是对于初学者来说非常常见的陷阱:
setup() {
function onClick() {
this // not the `this` you'd expect!
}
}
解开 Ref
我们可以将一个ref 值暴露给渲染上下文,在渲染过程中,Vue 会直接使用其内部的值,也就是说在模板中你可以把 {{ num.value }} 直接写为 {{ num }} ,但是在js中还是需要通过 num.value取值和赋值。
toRefs API 用来提供解决此约束的办法——它将响应式对象的每个 property 都转成了相应的 ref。
② 只读数据 readonly
对于不允许写的对象,不管是普通object对象、reactive对象、ref对象,都可以通过readonly方法返回一个只读对象
直接修改readonly对象,控制台会打印告警信息,不会报错
const state = reactive({
count: 0
})
const readonlyState = readonly(state)
// 监听只读属性,state.count修改后依然会触发readonlyState.count更新
watch(() => readonlyState.count, (newVal, oldVal) => {
console.log('readonly state is changed!')
setTimeout(() => {
// 修改只读属性会打印告警信息,但是不会报错
readonlyState.count = 666
}, 1000)
})
③ 计算属性 computed
2.x和3.x中的computed都支持getter和setter,写法一样,只是3.x中是组合函数式
// vue2.x
export default {
...
computed: {
totalCount() {
return this.count + this.num
},
doubleCount: {
get() {
return this.count * 2
},
set(newVal) {
this.count = newVal / 2
}
}
}
}
// vue3.x
import { reactive, ref, toRefs, computed } from 'vue'
export default {
name: 'Test',
setup () {
const state = reactive({
count: 0,
double: computed(() => {
return state.count * 2
})
})
const num = ref(0)
const addCount = function () {
state.count++
}
const addNum = function () {
num.value++
}
// only getter
const totalCount = computed(() => state.count + num.value)
// getter & setter
const doubleCount = computed({
get () {
return state.count * 2
},
set (newVal) {
state.count = newVal / 2
}
})
return {
...toRefs(state),
num,
totalCount,
doubleCount,
addCount,
addNum
}
}
}
④ 监听属性 watch & watchEffect
3.x和2.x的watch一样,支持immediate和deep选项,但3.x不再支持'obj.key1.key2'的"点分隔"写法;
3.x中watch支持监听单个属性,也支持监听多个属性,相比2.x的watch更灵活;
3.x中watchEffect方法会返回一个方法,用于停止监听;
watch跟watchEffect不同的地方在于,watchEffect注册后会立即调用,而watch默认不会,除非显示指定immediate=true,并且watchEffect可以停止监听
在 DOM 当中渲染内容会被视为一种“副作用”:程序会在外部修改其本身 (也就是这个 DOM) 的状态。我们可以使用
watchEffectAPI 应用基于响应式状态的副作用,并自动进行重应用。
// vue2.x
export default {
...
data () {
return {
...
midObj: {
innerObj: {
size: 0
}
}
}
},
computed: {
totalCount() {
return this.count + this.num
}
},
watch: {
totalCount(newVal, oldVal) {
console.log(`count + num = ${newVal}`)
},
'midObj.innerObj.size': {
// deep: true,
immediate: true,
handler(newVal, oldVal) {
console.log(`this.midObj.innerObj.size = ${newVal}`)
}
}
}
}
// vue3.x
import { reactive, ref, toRefs, computed, watch } from 'vue'
export default {
name: 'Test',
setup () {
const state = reactive({
count: 0,
double: computed(() => {
return state.count * 2
}),
midObj: {
innerObj: {
size: 0
}
}
})
const num = ref(0)
const addCount = function () {
state.count++
}
const addNum = function () {
num.value++
}
// only getter
const totalCount = computed(() => state.count + num.value)
// 监听单个属性
watch(() => totalCount.value, (newVal, oldVal) => {
console.log(`count + num = ${newVal}`)
})
// 监听单个属性, immediate
watch(() => totalCount.value, (newVal, oldVal) => {
console.log(`count + num = ${newVal}, immediate=true`)
}, {
immediate: true
})
// 监听单个属性, deep
watch(() => state.midObj, (newVal, oldVal) => {
console.log(`state.midObj = ${JSON.stringify(newVal)}, deep=true`)
}, {
deep: true
})
setTimeout(() => {
state.midObj.innerObj.size = 1
}, 2000)
// 监听多个属性
watch([num, () => totalCount.value], ([numVal, totalVal], [oldNumVal, OldTotalVal]) => {
console.log(`num is ${numVal}, count + num = ${totalVal}`)
})
// 副作用,会立即执行
let callTimes = 0
const stopEffect = watchEffect(() => {
console.log('watchEffect is called!')
const div = document.createElement('div')
div.textContent = `totalCount is ${totalCount.value}`
document.body.appendChild(div)
// 调用 5 次后,取消effect监听
callTimes++
if (callTimes >= 5) stopEffect()
})
return {
...toRefs(state),
num,
totalCount,
addCount,
addNum
}
}
}
4、生命周期钩子
2.x中生命周期钩子放在跟methods同级属性下
3.x中需要先导入钩子,然后在setup方法中注册钩子回调,并且钩子命名也跟React保持一样了
3.x移除了2.x中的beforeCreate和created钩子,通过setup方法代替
// vue2.x
export default {
data () {
return {}
},
methods: {
...
},
beforeCreate() {},
created() {},
beforeMount() {},
mounted() {},
beforeUpdate() {},
updated() {},
beforeDestroy() {},
destroyed() {}
}
// vue3.x
import {
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted
} from 'vue'
export default {
setup() {
onBeforeMount(() => {
console.log('component is onBeforeMount')
})
onMounted(() => {
console.log('component is onMounted')
})
onBeforeUpdate(() => {
console.log('component is onBeforeUpdate')
})
onUpdated(() => {
console.log('component is onUpdated')
})
onBeforeUnmount(() => {
console.log('component is onBeforeUnmount')
})
onUnmounted(() => {
console.log('component is onUnmounted')
})
}
}
2.x钩子对比3.x
5、Fragment
2.x中,vue template只允许有一个根节点
3.x中,vue template支持多个根节点,用过React的人应该知道<React.Fragment>和<></>
// vue2.x
<template>
<div>
<span>hello</span>
<span>world</span>
</div>
</template>
// vue3.x
<template>
<span>hello</span>
<span>world</span>
</template>
6、Teleport
teleport参照React中的portal,可以将元素渲染在父节点以外的其他地方,比如<body>下面的某个子元素
在vue3中, <teleport>是一个内置标签,我们通常将弹窗、tooltip等元素放在关闭的 </body>标签之前,如下:
<body>
<div id="app">
<!--main page content here-->
</div>
<!--modal here-->
</body>
如果按照以往的思路,需要将模态的UI代码放在底部,如下:
<body>
<div id="app">
<h3>Tooltips with Vue 3 Teleport</h3>
</div>
<div>
<my-modal></my-modal>
</div>
</body>
这样做是因为弹窗、tooltip需要显示在页面上层,需要正确处理父元素定位和z-index上下层级顺序,而最简单的解决方案是将这类DOM放在页面的最底部。这样的话这部分逻辑就脱离了整个项目的跟组件App的管理,就造成直接用JavaScript和CSS来修改UI,不规范并且失去响应式了。为了允许将一些UI片段段移动到页面中的其他位置,在Vue3中添加了一个新的<teleport>组件,并且<teleport>会在组件销毁时自动清空相应的dom,不用人工处理。
要使用 <teleport>,首先要在页面上添加一个元素,我们要将模态内容渲染到该元素下面。
<body>
<div id="app">
<h3>Tooltips with Vue 3 Teleport</h3>
</div>
<div id="endofbody"></div>
</body>
<template>
<button @click="openModal">
Click to open modal! (With teleport!)
</button>
<teleport to="#endofbody">
<div v-if="isModalOpen" class="modal">
...
</div>
</teleport>
</template>
<script>
import { ref } from 'vue'
export default {
setup() {
const isModalOpen = ref(false)
const openModal = function () {
isModalOpen.value = true
}
return {
isModalOpen,
openModal
}
}
}
</script>
7、Suspense
<Suspense>是一个特殊的组件,它将呈现回退内容,而不是对于的组件,直到满足条件为止,这种情况通常是组件setup功能中发生的异步操作或者是异步组件中使用。例如这里有一个场景,父组件展示的内容包含异步的子组件,异步的子组件需要一定的时间才可以加载并展示,这时就需要一个组件处理一些占位逻辑或者加载异常逻辑,要用到 <Suspense>,例如:
// vue2.x
<template>
<div>
<div v-if="!loading">
...
</div>
<div v-if="loading">Loading...</div>
</div>
</template>
或者在vue2.x中使用vue-async-manager
<template>
<div>
<Suspense>
<div>
...
</div>
<div slot="fallback">Loading...</div>
</Suspense>
</div>
</template>
// vue3.x
<Suspense>
<template >
<Suspended-component />
</template>
<template #fallback>
Loading...
</template>
</Suspense>
vue-router@4.x和vuex@4.x
vue2.x使用的是vue-router@3.x和vuex@3.x,vue3.x使用的是vue-router@4.x和vuex@4.x
1、vue-router@4.x
创建实例
// vue2.x router
import Vue from 'vue'
import Router from 'vue-router'
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
}
]
Vue.use(Router)
const router = new Router({
base: process.env.BASE_URL,
mode: 'history',
scrollBehavior: () => ({ y: 0 }),
routes
})
export default router
// vue3.x router
import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
}
]
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
})
export default router
scrollBehavior滚动行为
vue3.x router弃用了vue2.x router中的 { selector, x, y, offset },使用{ el, left, top, behavior }代替,新的api语义更接近原生DOM
// vue2.x router
const router = new Router({
base: process.env.BASE_URL,
mode: 'history',
scrollBehavior: () => ({ x: 0, y: 0 }),
routes
})
// vue3.x router
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes,
scrollBehavior(to, from, savedPosition) {
// scroll to id `#app` + 200px, and scroll smoothly (when supported by the browser)
return {
el: '#app',
top: 0,
left: 0,
behavior: 'smooth'
}
}
})
路由组件跳转
vue2.x使用路由选项redirect设置路由自动调整,vue3.x中移除了这个选项,将在子路由中添加一个空路径路由来匹配跳转
// vue2.x router
[
{
path: '/',
component: Layout,
name: 'WebHome',
meta: { title: '平台首页' },
redirect: '/dashboard', // 这里写跳转
children: [
{
path: 'dashboard',
name: 'Dashboard',
meta: { title: '工作台' },
component: () => import('../views/dashboard/index.vue')
}
]
}
]
// vue3.x router
[
{
path: '/',
component: Layout,
name: 'WebHome',
meta: { title: '平台首页' },
children: [
{ path: '', redirect: 'dashboard' }, // 这里写跳转
{
path: 'dashboard',
name: 'Dashboard',
meta: { title: '工作台' },
component: () => import('../views/dashboard/index.vue')
}
]
}
]
** 捕获所有路由:/:catchAll(.*) **
捕获所有路由 ( /* ) 时,现在必须使用带有自定义正则表达式的参数进行定义:/:catchAll(.*)
// vue2.x router
const router = new VueRouter({
mode: 'history',
routes: [
{ path: '/user/:a*' },
]
})
// vue3.x router
const router = createRouter({
history: createWebHistory(),
routes: [
{ path: '/user/:a:catchAll(.*)', component: component },
]
})
当路由为/user/a/b 时,捕获到的params 为 {"a": "a", "catchAll": "/b"}
** router.resolve **
router.match 与 router.resolve 合并在一起为 router.resolve,但签名略有不同
// vue2.x router
...
resolve ( to: RawLocation, current?: Route, append?: boolean) {
...
return {
location,
route,
href,
normalizedTo: location,
: route
}
}
// vue3.x router
function resolve(
rawLocation: Readonly<RouteLocationRaw>,
currentLocation?: Readonly<RouteLocationNormalizedLoaded>
): RouteLocation & { href: string } {
...
let matchedRoute = matcher.resolve(matcherLocation, currentLocation)
...
return {
fullPath,
hash,
query: normalizeQuery(rawLocation.query),
...matchedRoute,
redirectedFrom: undefined,
href: routerHistory.base + fullPath,
}
}
获取当前路由
删除 router.getMatchedComponents,可以从 router.currentRoute.value.matched 中获取
router.getMatchedComponents 返回目标位置或是当前路由匹配的组件数组 (是数组的定义/构造类,不是实例)。通常在服务端渲染的数据预加载时使用。
[{
aliasOf: undefined
beforeEnter: undefined
children: []
components: {default: {…}, other: {…}}
instances: {default: null, other: Proxy}
leaveGuards: []
meta: {}
name: undefined
path: "/"
props: ƒ (to)
updateGuards: []
}]
使用
如果使用 ,则可能需要等待router准备就绪才能挂载应用程序
app.use(router)
// Note: on Server Side, you need to manually push the initial location
router.isReady().then(() => app.mount('#app'))
一般情况下,正常挂载也是可以使用<transition>的,但是现在导航都是异步的,如果在路由初始化时有路由守卫,则在resolve 之前会出现一个初始渲染的过渡,就像给 <transiton> 提供一个 appear 一样
** 在服务端渲染 (SSR) 中,需要传递合适的 history mode **
const history = isServer ? createMemoryHistory() : createWebHistory()
const router = createRouter({ routes, history })
// on server only
router.push(req.url) // request url
router.isReady().then(() => {
// resolve the request
})
** push 或者 resolve 一个不存在的命名路由时,将会引发错误,而不是导航到根路由 "/" 并且不显示任何内容 **
在 vue2.x router 中,当 push 一个不存在的命名路由时,路由会导航到根路由 "/" 下,并且不会渲染任何内容。浏览器控制台只会打印警告,并且 url 会跳转到根路由 / 下。
const router = new VueRouter({
mode: 'history',
routes: [{ path: '/', name: 'foo', component: Foo }]
}
this.$router.push({ name: 'baz' })
在 vue3.x router 中,同样做法会引发错误。
const router = createRouter({
history: routerHistory(),
routes: [{ path: '/', name: 'foo', component: Foo }]
})
...
import { useRouter } from 'vue-next-router'
...
const router = userRouter()
router.push({ name: 'baz' })) // 这行代码会报错
** 获取路由 **
网上一些教程会告诉你通过ctx访问router和store对象,但是其实这种方式只能在develement模式有效,在production环境编译后,ctx在develement下看到的属性都无法访问,容易误导大家
** 错误示例: **
import { getCurrentInstance } from 'vue'
export default {
setup () {
const { ctx } = getCurrentInstance()
console.log(ctx)
console.log(ctx.$router.currentRoute.value)
const userId = computed(() => ctx.$store.state.app.userId)
return {
userId
}
}
}
** 正确使用: **
import { getCurrentInstance } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useStore } from 'vuex'
export default {
setup () {
const { ctx } = getCurrentInstance()
console.log(ctx)
const router = useRouter()
const route = useRoute()
const store = userStore()
console.log(router, route, store)
console.log(router.currentRoute.value)
const userId = computed(() => store.state.app.userId)
return {
userId
}
}
}
2、vuex@4.x
vuex4.x很少breaking change,整体改动较少
** 创建实例 **
// vue2.x vuex
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state: {},
mutations: {},
actions: {},
getters: {},
modules: {}
}
// vue3.x vuex
import Vuex from 'vuex'
export default Vuex.createStore({
state: {},
mutations: {},
actions: {},
getters: {},
modules: {}
})
** 获取store **
// vue3.x vuex
import { getCurrentInstance } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useStore } from 'vuex'
export default {
setup () {
const { ctx } = getCurrentInstance()
console.log(ctx)
const router = useRouter()
const route = useRoute()
const store = userStore()
console.log(router, route, store)
console.log(router.currentRoute.value)
const userId = computed(() => store.state.app.userId)
return {
userId
}
}
}
3.x跟2.x的其他差异
** ctx属性 **
对于网上一些其他文档使用
ctx.$router、ctx.$store访问router和store的应该小心避坑,注意开发环境和生产环境的差别
** vue3.x 生产环境 ctx **
生产环境的ctx,$router、$store没有了,其他属性也都没有了,不能通过ctx.$router、ctx.$store访问router和store,因此ctx可以说对我们没有用,应该避免在代码中使用ctx
执行顺序
vue3.x中会先执行setup方法,再执行兼容2.x的其他方法,比如data、computed、watch等,
并且在setup执行过程中,无法访问data中定义的属性,因为此时还未执行到data方法
mount挂载
使用mount挂载的时候
2.x会使用挂载元素的outerHTML作为template,并替换挂载元素
3.x会使用挂载元素的innerHTML作为template,并且只替换挂载元素的子元素
** this.$el、reactive refs、template refs **
2.x可以在组件挂载之后通过this.$el访问组件根元素
3.x去掉this,并且支持Fragment,所以this.$el没有存在的意义,建议通过refs访问DOM
当使用组合式 API 时,reactive refs 和 template refs 的概念已经是统一的。为了获得对模板内元素或组件实例的引用,我们可以像往常一样在 setup() 中声明一个 ref 并返回它
** 使用reactive refs和template refs **
<template>
<div ref="root"></div>
</template>
<script>
import { ref, onMounted, getCurrentInstance } from 'vue'
export default {
setup() {
const vm = getCurrentInstance()
const root = ref(null)
onMounted(() => {
// 在渲染完成后, 这个 div DOM 会被赋值给 root ref 对象
console.log(root.value) // <div/>
console.log(vm.refs.root) // <div/>
console.log(root.value === vm.refs.root) // true
})
return {
root
}
}
}
</script>
** 在v-for中使用 **
<template>
<div v-for="(item, i) in list" :key="i" :ref="el => { divs[i] = el }">
{{ item }}
</div>
</template>
<script>
import { ref, reactive, onBeforeUpdate } from 'vue'
export default {
setup() {
const list = reactive([1, 2, 3])
const divs = ref([])
// 确保在每次变更之前重置引用
onBeforeUpdate(() => {
divs.value = []
})
return {
list,
divs
}
}
}
</script>
** setup返回普通对象 **
setup返回普通对象的时候,会跟reactive对象一样,具备响应式,执行下面这段代码后会发现普通对象obj1.cnt也具有响应式了,虽然这样可以行得通,但是为了可读性,防止不了解这个特性的同学误解为非响应式的,建议还是通过reactive包一下。
<template>
<div>{{ obj1.cnt }}</div>
<div>{{ obj2.cnt }}</div>
</template>
<script>
import { reactive } from 'vue'
export default {
setup () {
// 普通对象
const obj1 = {
cnt: 1
}
// 代理对象
const obj2 = reactive({
cnt: 1
})
setInterval(() => {
obj1.cnt++
obj2.cnt++
}, 5000)
return {
obj1,
obj2
}
}
}
</script>
** directive指令 **
vue3.x对指令的生命周期钩子进行了改造,改造后更像3.x普通vue组件的钩子,更方便记忆
// vue2.x
export default {
name: 'YourDirectiveName',
bind(el, binding, vnode, oldVnode) {},
inserted(...) {},
update(...) {},
componentUpdated(...) {},
unbind(...) {}
}
// vue3.x
export default {
beforeMount(el, binding, vnode, oldVnode) {},
mounted(...) {},
beforeUpdate(...) {},
updated(...) {},
beforeUnmount(...) {},
unmounted() {...}
}
** render方法修改 **
vue、react都提供了render方法渲染html模板,直接使用render方法的还是比较少,毕竟有template和JSX,对于确实需要自定义render方法渲染模板内容的,具体变动如下:
// vue2.x
export default {
render(h) {
return h('div')
}
}
// vue3.x
import { h } from 'vue'
export default {
render() {
return h('div')
}
}
** 3.x中移除的一些特性 **
** 取消KeyboardEvent.keyCode **
在vue3.x中,给keyup事件配置一个指定按钮的keyCode(数字)将不会生效,但是依然可以使用别名,例如:
// 无效
<input @keyup.13="handler" />
// 有效
<input @keyup.enter="handler" />
** 移除on,off 和 $once方法 **
在Vue2.x中可以通过EventBus的方法来实现组件通信
// 声明实例
var EventBus = new Vue()
Vue.prototype.$globalBus = EventBus
// 组件内调用
this.$globalBus.$on('my-event-name', callback)
this.$globalBus.$emit('my-event-name', data)
在vue3.x中移除了$on、$off等方法,而是推荐使用mitt方案来代替:
// 声明实例
import mitt from 'mitt'
const emitter = mitt()
// 组件内调用
// listen to all events
emitter.on('*', (type, e) => console.log(type, e))
emitter.on('my-event-name', callback)
emitter.emit('my-event-name', data)
// clearing all events
emitter.all.clear()
** 移除filters **
在vue3.x中,移除了组件的filters项,可以使用methods的或者computed来替代
** 移除inline-template **
在Vue2.x中,在父组件引入子组件时,会用到inline-template来使子组件的内容也得到展示,参考这里,例如:
<my-component inline-template>
<div>
<p>These are compiled as the component's own template.</p>
<p>Not parent's transclusion content.</p>
</div>
</my-component>
在Vue3中,这个功能将被移除,目前inline-template使用的并不多,这里就不再过多讲解