Vue3.x 与 Vue2.x 区别、环境搭建、相关API的使用

5,896 阅读12分钟

关注公众号: 微信搜索 前端工具人 ; 收货更多的干货

一、 开篇

官方中文文档链接: https://vue3js.cn/docs/zh/

发布时间:2020年9月18号晚上,已正式发布 vue3.0 beta 版本

二、vue2.0 项目的建议

引用官方文档作者的话:

提示: 我们仍在开发 Vue 3 的专用迁移版本,该版本的行为与 Vue 2 兼容,运行时警告不兼容。如果你计划迁移一个非常重要的 Vue 2 应用程序,我们强烈建议你等待迁移版本完成以获得更流畅的体验

目前作者的意思是:对于 vue2.0 的项目强烈不建议升到 vue3.0;因为目前的beta版本以及现有的框架及插件,不是很支持和兼容vue3.0语法; 所以肯定有很多预想不到的问题;对于跃跃欲试升级vue3.0的小伙伴们,只能等待官方的兼容版本开发完,在做迁移; 毕竟线上项目不是开玩笑的,出了一个bug都可能是重大损失。 这个锅家里没矿的基本背不动...

三、介绍

Vue3带来些什么? 参考至: 公众号:前端早读课文章

详细文档请参考: 官方中文文档链接

  • 更快
    • 重构了Virtual DOM
      • 标记静态内容,并区分动态内容
      • 更新时只diff动态的部分
    • 重构了 双向数据绑定
      • Object.defineProperty() --> Proxy API
      • Proxy 对于复杂的数据结构减少了循环递归的监听;初始渲染循环递归是非常耗性能的;
      • Proxy 对于数组的变异方法(会修改原数组),不在需要单独用数组原生方法重写、处理
      • 语法也比defineProperty简洁多了,直接监听某个属性即可;
    • 事件缓存
      • vue2中,针对绑定事件,每次触发都要重新生成全新的function去更新;
      • Vue3中,提供了事件缓存对象cacheHandlers,当cacheHandlers开启的时候,编译会自动生成一个内联函数,将其变成一个静态节点,这样当事件再次触发时,就无需重新创建函数直接调用缓存的事件回调方法即可
  • 更小 (Tree shaking支持)
    • 简而言之: 不会把所有的都打包进来,只会打包你用到的api;大项目你会发现热加载、初始渲染提升了很多
    • 很大程度的减少了开发中的冗余代码,提升编译速度
  • 更易于维护
    • Vue3Flow迁移到TypeScript
      • 多人协同开发的情况下,用了 TypeScript 之后的酸爽你会吐槽,为什么早不出现 TypeScript
    • 代码目录结构遵循monorepo
      • 核心观点: 代码分割到一个个小的模块中, 开发者大部分只是工作在少数的几个文件夹,并且也只会编译自己负责的模块;而不是整个项目编译
  • 新功能和特性 Composition API
    • 不要在意越来越像react-hook;毕竟别人的优点是值得自己学习的;
    • Composition API 函数式开发,很大程度的提高组件、业务逻辑的复用性;高度解耦;提升代码质量、开发效率;减少代码体积
  • 提升开发效率 vite 的支持 (当然目前来说 vite 功能还不够强大和稳定, 但尤大把它作为vue3官方构建工具,那肯定尤大会完善它的; 可自由选择webpack还是vite
    • vite在开发环境下基于浏览器原生 ES imports 开发,在生产环境下基于Rollup打包
      • 快速的冷启动
      • 即时的模块热更新
      • 真正的按需编译
    • vue2.0 相信很多小伙伴都是结合webpack开发; 但是有没有发现初期项目小的时候很爽运行、编译、热加载都很快; 项目一大... 打包、运行、改个功能热加载的时候... 我们先去上个厕所/接个水;忙的时候挺烦这环节
    • vue3.0 结合 vite作者的介绍是不跟项目体积庞大而影响,开始啥样现在也啥样; 当然夸张是夸张了点, 但是相差应该不大;

四、对 vue2 的变化

4.1. 生命周期的变化
  • 改成了按需引入, 大部分需要 + on;
  • vue3取消beforeCreate、 created了这两个钩子,统一用setup代替;
  • beforeDestroy -> onBeforeUnmount; destroyed -> onUnmounted;
4.2. 多根节点

vue2.x 需要去将组件包裹在 div 中,否则报错;

4.3. 异步组件
  • Vue3 提供 Suspense组件,允许等待异步组件时渲染内容;
  • 附带两个命名插槽:default和fallback。Suspense确保加载完异步内容时显示默认插槽,并将fallback插槽用作加载状态;
4.4. Teleport
  • vue3 提供Teleport组件可将部分DOM移动到 Vue app之外的位置。比如项目中常见的Dialog组件
4.5. Composition 组合式API 及 新增了Hooks
  • Vue2 是 选项式API(Option API),一个逻辑会散乱在文件不同位置(data、props、computed、watch等),导致代码的可读性变差,需要上下来回跳转文件位置;
  • Composition API 增强了代码的可读性、内聚性,组合式API 还提供了较为完美的逻辑复用性方案
  • 整合业务代码逻辑,提取公共逻辑(vue2采用mixin-命名冲突数据来源不清晰)
4.6. 数据劫持优化
  • Vue2 针对常用数组原型方法push、pop、shift、unshift、splice、sort、reverse进行了hack处理;提供Vue.set监听对象/数组新增属性。对象的新增/删除响应,还可以new个新对象,新增则合并新属性和旧对象;删除则将删除属性后的对象深拷贝给新对象
  • Vue3.x改用 Proxy 替代 Object.defineProperty
  • Proxy只会代理对象的第一层,Vue3是怎样处理这个问题的呢?
    • 判断当前Reflect.get的返回值是否为Object,如果是则再通过reactive方法做代理, 这样就实现了深度观测。
    • 监测数组的时候可能触发多次get/set,那么如何防止触发多次呢?判断key是否为当前被代理对象target自身属性,也可以判断旧值与新值是否相等,只有满足以上两个条件之一时,才有可能执行trigger。
  • Proxy 与 Object.defineProperty 优劣对比
    • Proxy 可以直接监听对象而非属性 (支持 Map、Set、WeakMap 和 WeakSet)
    • Proxy 可以直接监听数组的变化
    • Proxy 有多达 13 种拦截方法,不限于 apply、ownKeys、deleteProperty、has 等等是 Object.defineProperty 不具备的;
    • Proxy 返回的是一个新对象,我们可以只操作新的对象达到目的,而 Object.defineProperty 只能遍历对象属性直接修改;
    • Proxy 作为新标准将受到浏览器厂商重点持续的性能优化,也就是传说中的新标准的性能红利
    • Object.defineProperty 兼容性好,支持 IE9,而 Proxy 的存在浏览器兼容性问题,而且无法用 polyfill 磨平
    • Proxy一定要配合Reflect使用。恰恰是为什么触发代理对象的劫持时保证正确的 this 上下文指向
4.7. 编译优化 虚拟DOM
  • Vue3 相比于 Vue2 虚拟DOM 上增加patchFlag字段, 值 1 代表节点为动态文本节点,那在 diff 过程中,只需比对文本对容,无需关注 class、style等。除此之外,发现所有的静态节点,都保存为一个变量进行静态提升,可在重新渲染时直接引用,无需重新创建
4.8. 事件缓存

Vue3 的 cacheHandler可在第一次渲染后缓存我们的事件。相比于 Vue2 无需每次渲染都传递一个新函数

4.9. Diff 优化

vue3实现了静态模板分析,重写了diff算法; 增加 patchFlag帮助 diff 时区分静态节点,以及不同类型的动态节点。一定程度地减少节点本身及其属性的比对

4.10. 源码体积优化、打包优化
  • tree-shaking:模块打包webpack、rollup等中的概念。移除 JavaScript 上下文中未引用的代码。主要依赖于import和export语句,用来检测代码模块是否被导出、导入,且被 JavaScript 文件使用; Vue 应用程序中未使用的api将从最终的捆绑包中消
  • 引入tree-shaking:通过编译阶段的静态分析,找到没有引入的模块并打上标记,然后在压缩阶段会删除这些没有用的代码
  • 如果在项目中没有引入transition、keepAlive等组件,那么他们对应的代码就不会打包,从而减少项目引入vue.js包体积的目的 除
4.11. 源码方向

vue2.0采用flow进行编写,而3.0源码全部采用Ts进行开发,对TS支持友好

4.12. 作用域插槽

2.x 的机制导致作用域插槽变了,父组件会重新渲染,而 3.0把作用域插槽改成了函数的方式,这样只会影响子组件的重新渲染,提升了渲染的性能

4.13. 新增Fragment、Teleport、Suspense 组件

五、 环境搭建

// 对于 Vue 3,应该 npm 上可用的 Vue CLI v4.5 作为 @vue/cli@next
yarn global add @vue/cli@next
# OR
npm install -g @vue/cli@next

// 创建项目
npm init vite-app <project-name>
# OR
yarn create vite-app <project-name>

// 下载依赖及运行项目, 已 npm 方式为例; 详细步骤官方文档的安装页都有
cd <project-name>
npm install
npm run dev // 项目就能跑起来并且访问了

拓展: 项目引入其他插件比如 vue-router4.0、vuex4.0、typescript等请参考 Vue3.0环境搭建

六、 语法介绍

上手之前应该先阅读一遍 vue3 对于 vue2 的一些变更; 详情点击 官网文档重大变更

vue2中使用的是Options API; vue3中的是Composition API 简称纯函数式API

6.1、 setup

vue3 组件入口为 setup(){} 函数作为入口, 默认只执行一次;执行顺序在 beforeCreate 之后 created 之前;

...
// 使用props和this
setup (props, ctx) {
  // props 组件间传递的参数; 
  // ctx 组件的实例的执行上下文(可以理解为 vue2 this) 
     /* 可执行 下面等操作:例 ctx.$emit() 
     attrs: Object
     emit: ƒ ()
     listeners: Object
     parent: VueComponent
     refs: Object
     root: Vue 
     */
  // 注意 steup 中没有this了, 拿不到this
}

6.2、 生命周期

我记得早期是说 vue3 中是移除掉了 beforeCreatecreated两个生命周期; 但是实践的时候我发现还是可以写的; 因为vue2vue3 写法目前相兼容;

created () { console.log('created') }
setup (props, ctx) {
  console.log('setup')
  // mounted 新写法 记住一句话 所有的方式都是以函数的形式呈现
  onMounted(() => {})
}
mounted () { console.log('mounted') }
// 执行顺序 setup created mounted

虽然兼容但尽量不要这样写;向前看齐嘛; 强烈推荐全部都放在steup函数中

6.3、 reactive、ref、toRefs、isRef

创建响应式对象 reactive、ref、toRefs 用法, 对应 vue2 中的 data 推荐写法3

// 写法一:响应式数据一多, return 要很多次; 使用数据的时候要通过state拿到
<template>
  <div>
    <p>{{state.count}}</p>
  </div>
</template>
import {reactive} from 'vue'
...
setup(props, ctx) {
  const state = reactive({ 
    count: 0
  })
  return { state }
}
// 写法二
<template>
  <div>
    <p>{{count}}</p>
  </div>
</template>
import {reactive} from 'vue'
...
setup(props, ctx) {
  const state = reactive({ 
    count: 0
  })
  return {
    count: state.count
  }
}
// 写法三:推荐 通过 toRefs 代理对象, 再通过解构的方式取值
<template>
  <div>
    <p>{{count}}</p>
  </div>
</template>
import {reactive, toRefs} from 'vue'
...
setup(props, ctx) {
  const state = reactive({ 
    count: 0
  })
  return {
    ...toRefs(state)
  }
}
// 写法四:通过 ref() 函数包装, 返回值是一个对象,对象上只包含一个 value 属性, 就是要的属性值
<template>
  <div>
    <p>{{count}}</p>
    <p>{{count1}}</p>
  </div>
</template>
import {reactive, toRefs, ref} from 'vue'
...
setup(props, ctx) {
  // 父组件传递count属性
  // 写法1
  const count = ref(props.count)
  console.log(count.value) // 对应props.count的值
  // 写法2 
  const state = reactive({ 
    count1: ref(props.count)
  })
  return {
    count,
    ...toRefs(state)
  }
}
// isRef 来判断某个值是否为 ref() 创建出来的对象
import { ref, isRef } from 'vue';
export default {
  setup(props, ctx) {
    const refCount = ref(0)
    const count = isRef(refCount) ? refCount : 1
  }
};

6.4、 computed

例子场景:结合 vue-router 根据当前路劲为count赋值, 也扩展下vue-router的用法

<template>
  <div>
    <p>{{count}}</p>
    <p>{{count1}}</p>
  </div>
</template>
import {reactive, toRefs, computed} from 'vue'
import {useRoute} from 'vue-router'
...
setup(props, ctx) {
  const route = useRoute()
  const state = reactive({ 
    // 计算属性 写法1
    count: computed(() => {
      return route.path
    })
  })
  // 计算属性 写法2
  const count1 = computed(() => {
    return route.path
  })
  return {
    ...toRefs(state),
    // 计算属性不需要通过 toRefs 结构, 因为他就是一个具体的值就是响应式的
    count1
  }
}

6.5、watch 、watchEffect

例子场景:同 computed 一样

watchEffectwatch 有什么不同:

  1. watchEffect 不需要指定监听的属性,他会自动的收集依赖, 只要我们回调中引用到了 响应式的属性, 那么当这些属性变更的时候,这个回调都会执行
  2. watch 只能监听指定的属性
  3. watch 可以获取到新值与旧值,而 watchEffect 不行
  4. watchEffect 在组件初始化的时候就会执行一次用以收集依赖(与computed同理),后续收集的依赖发生变化,这个回调才会再次执行
// watch 用法 监听单个属性
<template>
  <div>
    <p>{{count}}</p>
  </div>
</template>
import {reactive, toRefs, watch} from 'vue'
import {useRoute} from 'vue-router'
...
setup(props, ctx) {
  const route = useRoute()
  const state = reactive({ 
    count: 0,
  })
  // 监听路由路劲, immediate 是否立即执行一次
  watch(() => route.path, (newValue) => {
    state.count = newValue
  }, { immediate: true })
  
  return {
    ...toRefs(state),
  }
}
// watch 用法 监听ref数据源
<template>
  <div>
    <p>{{count}}</p>
  </div>
</template>
import {reactive, toRefs, ref, watch} from 'vue'
...
setup(props, ctx) {
  // 定义数据源
  let count = ref(0);
  // 指定要监视的数据源
  watch(count, (count, prevCount) => {
    console.log(count, prevCount)
  })
  setInterval(() => {
    count.value += 2
  }, 2000)
  console.log(count.value)
  return {
    count
  }
}
// watch 用法 监听多个属性
<template>
  <div>
    <p>{{count}}</p>
  </div>
</template>
import {reactive, toRefs, watch} from 'vue'
...
setup(props, ctx) {
  const state = reactive({ 
    name: 'vue',
    age: 3
  })
  watch(
    // 监听name、 age
    [() => state.name, () => state.age],
    // 如果属性改变、则执行以下回调
    ([newName, newAge], [oldname, oldAge]) => {
      console.log(oldname, oldname)
      console.log(oldAge, oldAge)
    },
    { lazy: true} // 在 watch 被创建的时候,不执行回调函数中的代码
  )
  setTimeout(() => {
    state.name = 'react'
    state.age += 1
  }, 3000)
  return {
    ...toRefs(state),
  }
}
// watchEffect 用法
<template>
  <div>
    <p>{{count}}</p>
  </div>
</template>
import {reactive, toRefs, ref, watchEffect} from 'vue'
import {useRoute} from 'vue-router'
...
setup(props, ctx) {
  const route = useRoute()
  const state = reactive({ 
    count: 0,
  })
  // 当 route.path 变化时就会执行打印, 有点类似 react-hook 的 useEffect 第二个参数效果
  watchEffect(() => {
    count = route.path
    console.log(route.path)
  })
  // watchEffect、 watch 都可以主动停止监听
  const stop = watchEffect(() => { 
    count = route.path
    console.log(route.path)
  })
  // 在某个时机下 执行 stop() 停止watchEffect监听
  if (...) { stop() }
  return {
    ...toRefs(state),
  }
}

七、Vue3.2 语法介绍

7.1. 变量、方法不需要 return 出来
7.2. 组件不需要在注册
// 组件命名采用的是大驼峰,引入后不需要在注册, 在使用的使用直接是小写和横杠的方式连接
import TestCom from "../components/TestCom.vue" ===> <test-com></test-com>
7.3. 新增 defineProps
const props = defineProps({
    info:{
        type:String,
        default:'----'
    },
    time:{
        type:String,
        default:'0分钟'
    },
})
7.4. 新增 defineEmits
let $myemit = defineEmits(['myAdd','myDel'])
let hander1Click=() :void=>{
    $myemit('myAdd','新增的数据')
}

let hander2Click=() :void=>{
    $myemit('myDel','删除的数据')
}
7.5. defineExpose 获取子组件中的属性值
// 将组件中的属性暴露出去,这样父组件可以获取
defineExpose({
    sex,
    info
})
7.6. 新增指令 v-memo
  • v-memo会记住一个模板的子树,元素和组件上都可以使用。
  • 该指令接收一个固定长度的数组作为依赖值进行[记忆比对]。
  • 如果数组中的每个值都和上次渲染的时候相同,则整个子树的更新会被跳过。
  • 即使是虚拟 DOM 的 VNode 创建也将被跳过,因为子树的记忆副本可以被重用。
7.7. style v-bind
<style scoped>
  span {
    /* 使用v-bind绑定state中的变量 */
    color: v-bind('state.color');
  }  
</style>
7.8. 辅助函数
  • useAttrs: 获取 attrs 数据
  • useSlots: 获取 slots 插槽数据
  • useCssModule: 类似于 react 的 className 语法
7.9. 顶层await支持

必须与 Suspense 组合使用

7.10. 指令
// 局部自定义指令
// 必须以 小写字母v开头的小驼峰 的格式来命名本地自定义指令
// 在模板中使用时,需要用中划线的格式表示,不可直接使用vMyDirective
const vMyDirective = {
  beforeMount: (el, binding, vnode) => {
    el.style.borderColor = 'red'
  },
  updated(el, binding, vnode) {
    if (el.value % 2 !== 0) {
      el.style.borderColor = 'blue'
    } else {
      el.style.borderColor = 'red'
    }
  },
}
// 自定义全局指令,可以在main.js 的app上注册就可以全局使用
app.directive("focus", {
  mounted(el, bindings, vnode, preVnode) {
    console.log("focus mounted");
    el.focus();
  }
})
// 指令生命周期
created:在绑定元素的 attribute 或事件监听器被应用之前调用;
beforeMount:当指令第一次绑定到元素并且在挂载父组件之前调用;
mounted:在绑定元素的父组件被挂载后调用;
beforeUpdate:在更新包含组件的 VNode 之前调用;
updated:在包含组件的 VNode 及其子组件的 VNode 更新后调用;
beforeUnmount:在卸载绑定元素的父组件之前调用;
unmounted:当指令与元素解除绑定且父组件已卸载时,只调用一次

7.11. 命名空间组件
// components/Form/index.js
import Form from './Form.vue'
import Input from './Input.vue'
import Label from './Label.vue'
// 把InputLabel组件挂载到 Form 组件上
Form.Input = Input
Form.Label = Label

export default Form

八、 Vue3中移除的一些API和方法

8.1 取消KeyboardEvent.keyCode

Vue2.x中,绑定键盘事件会用到如下代码:

<!-- keyCode version -->
<input v-on:keyup.13="submit" />

<!-- alias version -->
<input v-on:keyup.enter="submit" />

或者是:

Vue.config.keyCodes = {
  f1: 112
}
<!-- keyCode version -->
<input v-on:keyup.112="showHelpText" />

<!-- custom alias version -->
<input v-on:keyup.f1="showHelpText" />

在事件中,给keyup配置一个指定按钮的keyCode(数字)在Vue3中将不会生效,但是依然可以使用别名,例如:

<input v-on:keyup.delete="confirmDelete" />

8.2 移除 on,on,off 和 $once方法

Vue2.x中可以通过EventBus的方法来实现组件通信:

var EventBus = new Vue()
Vue.prototype.$EventBus = EventBus
...
this.$EventBus.$on()  this.$EventBus.$emit()

这种用法在Vue3中就不行了,在Vue3中移除了 $on,$off等方法(参考rfc),而是推荐使用mitt方案来代替:

import mitt from 'mitt'
const emitter = mitt()
// listen to an event
emitter.on('foo', e => console.log('foo', e) )
// fire an event
emitter.emit('foo', { a: 'b' })

8.3 移除filters

Vue3中,移除了组件的filters项,可以使用methods的或者computed来进行替代:

<template>
  <p>{{ accountBalance | currencyUSD }}</p>
</template>
<script>
  export default {
    filters: {
      currencyUSD(value) {
        return '$' + value
      }
    }
  }
</script>

替换为:

<template>
  <p>{{ accountInUSD }}</p>
</template>
<script>
  export default {
    props: {
      accountBalance: {
        type: Number,
        required: true
      }
    },
    computed: {
      accountInUSD() {
        return '$' + this.accountBalance
      }
    }
  }
</script>

九、Vue3中改变的API和写法

9.1 实例初始化

vue2.x中通过new Vue()的方法来初始化:

import App from './App.vue'
new Vue({
  store,
  render: h => h(App)
}).$mount('#app')

vue3Vue不再是一个构造函数,通过createApp方法初始化:

import App from './App.vue'
createApp(App).use(store).mount('#app')

9.2 全局API调用方式改变

Vue2.x中,大部分全局API都是通过Vue.xxx或者Vue.abc()方式调用,例如:

  import Vue from 'vue'
  Vue.mixin()
  Vue.use()

而在Vue3中,这些方式将会改变,取而代之的是如下:

  import { createApp } from 'vue'
  const app = createApp({})
  app.mixin()
  app.use()

同时,可以只引入一些需要的API,不需要的不用引入,这样也符合Three Shaking的要求,例如:

  import { nextTick,reactive,onMounted } from 'vue'
  nextTick(() => {
  })
  onMounted(() => {
  })

由于Vue3中全局API都会通过app.xxx的方法调用,所以之前通过Vue.prototype.xxx绑定的全局方法和变量将无法使用,可以采用如下方式来代替:

  //在main.js中:
  app.config.globalProperties.http = function(){}

  //在vue组件中:
  this.http()

9.3 render方法修改

Vue2.x中,有时会自定义render方法来返回模板内容,如下:

export default {
  render(h) {
    return h('div')
  }
}

Vue3中,h通过vue来引入,如下:

import { h } from 'vue'
export default {
  render() {
    return h('div')
  }
}

9.4 新的异步组件创建方式

Vue2.x中,尤其是在Vue Router中,会经常使用到异步组件,借助webpack的打包方式,可以将一个组件的代码进行异步获取,例如:

const asyncPage = () => import('./NextPage.vue')
const asyncPage = {
  component: () => import('./NextPage.vue'),
  delay: 200,
  timeout: 3000,
  error: ErrorComponent,
  loading: LoadingComponent
}

Vue3中,提供了defineAsyncComponent()方法创建异步组件,同时可以返回一个Promise对象来自己控制加载完成时机,如下:

import { defineAsyncComponent } from 'vue'
const asyncPageWithOptions = defineAsyncComponent({
  loader: () => import('./NextPage.vue'),
  delay: 200,
  timeout: 3000,
  error: ErrorComponent,
  loading: LoadingComponent
})
const asyncComponent = defineAsyncComponent(
  () =>
    new Promise((resolve, reject) => {
      /* ... */
    })
)

十、参考链接