vue3.0

504 阅读7分钟

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的大部分语法,包括现在的datamethodscomputedwatchhooks等,但并不是全部兼容,另外还包含一些其他修改,需要手动修改支持,所以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.productionTipVue.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方法中实现,包括datawatchcomputedmethodshooks,并且不再有this

vue3.x setup方法在组件生命周期内只执行一次,不会重复执行

相比vue2.x中基于OPTIONS配置的方式,vue3.x基于组合式API的方式语义没有2.x清晰,2.x中datamethodscomputedwatch等都通过不同的scope区分开,看起来很清晰,3.x都放在setup方法中,对代码组织能力会有更高的要求。

vue2.x使用Composition API可以安装@vue/composition-api,使用基本跟Composition API一样,这里不再赘述

① 状态和事件绑定 reactive & ref

reactive 几乎等价于 2.x 中的 Vue.observable() API,只是为了避免与 RxJS 中的 observable 混淆而做了重命名

vue3.xreactiveref取代了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.x3.x中的computed都支持gettersetter,写法一样,只是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.x2.xwatch一样,支持immediatedeep选项,但3.x不再支持'obj.key1.key2'的"点分隔"写法;

3.xwatch支持监听单个属性,也支持监听多个属性,相比2.xwatch更灵活;

3.xwatchEffect方法会返回一个方法,用于停止监听;

watchwatchEffect不同的地方在于,watchEffect注册后会立即调用,而watch默认不会,除非显示指定immediate=true,并且watchEffect可以停止监听

在 DOM 当中渲染内容会被视为一种“副作用”:程序会在外部修改其本身 (也就是这个 DOM) 的状态。我们可以使用 watchEffect API 应用基于响应式状态的副作用,并自动进行重应用。

// 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中的beforeCreatecreated钩子,通过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的管理,就造成直接用JavaScriptCSS来修改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.xvuex@4.x

vue2.x使用的是vue-router@3.xvuex@3.xvue3.x使用的是vue-router@4.xvuex@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.matchrouter.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.$routerctx.$store访问routerstore的应该小心避坑,注意开发环境和生产环境的差别

** vue3.x 生产环境 ctx ** 生产环境的ctx$router$store没有了,其他属性也都没有了,不能通过ctx.$routerctx.$store访问routerstore,因此ctx可以说对我们没有用,应该避免在代码中使用ctx

执行顺序

vue3.x中会先执行setup方法,再执行兼容2.x的其他方法,比如datacomputedwatch等,

并且在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 refstemplate 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使用的并不多,这里就不再过多讲解