基于Vue cli的Vue3.0初体验

前言

Vue3.0的步伐越来越近了,是时候了解起来了,虽然嘴上还喊学不动了,但是,身体还得诚实起来,接着学。。。
通过各种博客资料,还有前段时间尤雨溪大佬的直播Vue3相对Vue2的比较大的变化有以下几种:

  • 使用 TypeScript
  • 放弃 class 采用 function-based API
  • option API => Composition API
  • 重构 complier
  • 重构 virtual DOM
  • 新的响应式机制

使用ts的话就是抛弃了谷歌的flow选择拥抱微软的ts。从这个情况也看得出来,不会ts的该学起来了,比如我。。。

放弃class采用function-based API据说也是为了更好支持ts,为了更灵活的逻辑复用能力,代码更容易压缩等...

option Api到Composition Api应该是对我们去写代码影响最大的一部分,稍后可以看代码体验一下。

重构compiler与virtual DOM使Vue变得更快,也是Vue越来越优秀的原因。

新的响应式机制采用了ES6的ProxyApi,抛弃了之前的Object.defineProperty()比较直观的解决的是Vue2中这两点问题:

  • 关于对象:Vue 无法检测 property 的添加或移除。由于 Vue 会在初始化实例时对 property 执行 getter/setter 转化,所以 property 必须在 data 对象上存在才能让 Vue 将它转换为响应式的。

  • 关于数组:Vue 不能检测以下数组的变动:

    1. 当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue
    2. 当你修改数组的长度时,例如:vm.items.length = newLength

在官网深入响应式原理一章有较详细阐述,针对以上两种情况解决方法,官网也有给出答案,那就是使用set方法。

Proxy可以完美的解决该问题,当然好处应该不止这些,剩下的慢慢探究吧,Proxy也有缺点,那就是兼容性问题,有一些浏览器不支持,而且无完全polyfill,浏览器支持程度可以查看https://caniuse.com/#search=Proxy

简单了解Proxy

Vue核心就是响应式数据,Vue3.0中的响应式采用了Proxy那就简单看看Proxy是怎么个样子的呢。

Proxy 对象用于定义基本操作的自定义行为(如属性查找、赋值、枚举、函数调用等)。。。
from MDN,学习一个新的Api官方文档还是要读一下哈,虽然读不懂这么深奥的描述。。。

语法:

const p = new Proxy(target, handler)

参数target表示要使用Proxy包装的对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)

参数handler是一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为

看看代码吧:

let obj = {
    a: 1,
    b: 2
}
const proxy = new Proxy(obj, {
    get: function(target, prop, receiver) {
        return prop in target ? target[prop] : 0
    },
    set: function(target, prop, value, receiver) {
        target[prop] = 666
    }
})
console.log(proxy.a) // 1
console.log(proxy.c) // 0
proxy.a = 10
console.log(proxy.a) // 666
obj.b = 10
console.log(proxy.b) // 不是666 而是10 

以上代码中obj是我们要代理的目标对象,getset方法是参数handler的两个属性,具体如下:

handler.get()接收三个参数,第一个参数target为代理的目标对象,第二个参数prop是代理的目标对象的属性,第三个参数是Proxy或者继承Proxy的对象,通常是proxy本身。

handler.set()接收四个参数,其中三个参数都与get方法相同,唯独多出来一个value表示新的属性值。

上述代码表示当访问proxy的属性时,进行拦截判断,该属性是否是目标对象的属性,如果是那么就将其值返回出来,否则就返回0。

当对proxy上的属性进行重写时,将重写的该属性赋值为666。

注意:此时对数据的劫持,只是劫持了代理对象proxy,而跟原对象obj没有任何关系,对obj进行操作,也不会监听到。

proxy实现一个简易版的数据响应:

<body>
    <h2 id="app"></h2>
  <input id="input" type="text" />
</body>
let app = document.getElementById('app')
let input = document.getElementById('input')

let obj = { // 源数据
  text:'hello world'
}

let proxy = new Proxy(obj, {
  set: function(target, prop, value){ // input事件触发进行劫持,触发update方法
    target[prop] = value
    update(value)
  }
})

function update(value){ // update方法用于同步dom更新
    app.innerHTML = value
    input.value = value
}

input.addEventListener('input',function(e){ // 监听input数据变化,并修改proxy的值
  proxy.text = e.target.value
})

proxy.text = obj.text // 初始化源数据

使用Vue CLI体验Vue3.0

第一步,安装vue-cli

npm install -g @vue/cli

安装完成后查看是否已安装成功

vue -V
@vue/cli 4.4.4

如果cli已安装需要注意其版本应该高于cli4.x。

第二步,初始化vue项目

vue create vue-next-test

输入命令后,出现命令行交互,跟之前一样,主要是在初始时勾选上vue-router,vuex,避免在升级vue3的过程中手写初始化代码,会自动生成初始化代码。
注意:vue3.0项目目前不能直接创建,需从vue2.x升级。

vue-cli创建

第三步,升级成Vue3.0项目
以上只是创建了Vue2.x的项目,需要手动升级成Vue3.0的项目
进入vue-next-test文件夹cd vue-next-test
输以下指令:

vue add vue-next

执行上述指令会自动安装vue-cli-plugin-vue-next插件,该插件会自动完成以下操作

  • 安装vue3.0beta版依赖
  • 配置webpack去在vue3中编译.vue文件
  • 自动迁移全局api(创建新模板)
  • 升级安装Vue Router4.0和Vuex 4.0,如果默认为未安装,则不升级。
  • 自动修改Vue Router 和Vuex模板代码

升级完成之后就可以看代码啦!

第四步,查看Vue3.0的部分新的东西

  • Vuex对比

3.0版本Vuex

import Vuex from 'vuex'
export default Vuex.createStore({
  state: {
  },
  mutations: {
  },
  actions: {
  },
  modules: {
  }
})

2.x版Vuex

import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
export default new Vuex.Store({
  state: {
  },
  mutations: {
  },
  actions: {
  },
  modules: {
  }
})

Vue2.x版本采用构造函数构建Vue Router实例,而Vue3.0使用createStore方法来构建Vue实例,Vuex语法和Api基本没有发生变化。和之前一样,该怎么样写state,mutations等还是怎么写,该怎么调还怎么调。

  • Vue Router对比

3.0版本

import { createRouter, createWebHistory } from 'vue-router'
import Home from '../views/Home.vue'
const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
  },
  {
    path: '/test',
    name: 'test',
    component: () => import('../views/Test.vue')
  }
]

const router = createRouter({
  history: createWebHistory(),
  routes
})
export default router

2.x版本

import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
  },
  {
    path: '/test',
    name: 'test',
    component: () => import('../views/Test.vue')
  }
]
const router = new VueRouter({
  mode:"history",
  routes
})
export default router

同样的Vue Router也是之前采用构造函数形式,3.0采用createRouter方法去创建Vue Router实例,配置方法都一样。在路由模式配置上,之前是配置mode option,3.0则是采用vue-router中的createWebHistory方法去创建history属性,我默认选择的是history模式用的是createWebHistory方法创建history属性,如果要修改为hash模式则需要使用createWebHashHistory方法来创建。

总结:总的来说,构建Vue Router和Vuex的方式变了,但是它们的配置方式都和之前保持一致,可以无缝衔接使用。

  • Composition API
    在3.0代码基础上继续往下看,创建一个新的组件<Test/>,在<Test/>组件中认识一下Composition API
    之前2.x版本是采用了Options API的模式,可以理解为选项式的组件代码编写,Vue官方规定好的写法,响应式数据,methods,computed,components以及生命周期这些都是规定好的,需要在哪里写,你就得在哪里写。
<script>
  export default {
    data:() => {
      return {}
    },
    methods:{
    },
    computed:{
    },
    component:{
    },
    mounted(){
    }
  }
</script>

Vue3.0采用Composition API的模式,可以理解成组合API,怎么个组合法呢?就类似于,在组件中实现的这些东西,响应式数据,生命周期,计算属性等,都可以在Vue中获取对应方法,然后在一个方法中组合起来统一对外输出。
Composition API提供了以下一些函数,

  • ref
  • reactive
  • toRefs
  • computed
  • watch
  • getCurrentInstance
  • 生命周期hooks
  • ...
    在体验Composition API之前需要认识一个函数叫做setup(),这个函数的主要功能是Composition API的入口,它在生命周期beforeCreate生命周期执行之前被调用,接收props对象作为第一个参数,接收来的props对象,可以通过watch监视其变化。接受context对象作为第二个参数,这个对象包含attrs,slots,emit三个属性。多说无益,直接看代码吧。
import { ref, reactive } from 'vue'
export default {
  setup(props, context){
    const count = ref(0) // 定义响应式数据count
    const num = ref(1) // 定义响应式数据num
    const objData = {
      name: 'erha',
      age: '1',
      skill: '拆家'
    }
    const obj = reactive(objData) // 定义响应式数据obj
    return {
      count,
      num,
      obj
    }
  },
  name:'test'
}

在Vue3.0中创建响应式数据需要引用ref,reactive这两个方法,ref一般用来创建基本数据类型的响应式数据,reactive一般用来创建引用数据类型的响应式数据。
在模板中使用,跟之前没有区别,需要注意的是,ref属于将基本类型数据包装成应用类型,在模板中正常使用。在方法中访问的时候需要带上.value才能访问到。
以下代码我简写了,比如有一个按钮点击会触发一个方法,该方法是让count自增,那么应该这样写:

setup(props, context){
  const count = ref(0)
  const addCount = () => {
    count.value ++
  }
  return {
    count,
    addCount
  }
}

为什么要这么写呢?是因为Proxy的原因,Proxy要进行数据劫持的时候需要接收一个对象,所以ref就对基本数据类型的数据进行了包装,使其可以进行响应式。在方法中需要使用count.value去操作,而在模板中进行了处理,所以可以直接使用count进行渲染。

由于Proxy的机制原因,如果将reactive中的响应式数据进行解构,那么原先的响应式数据就变成不可响应的了。

import { reactive } from 'vue'
const data =reactive({
  name:'lisa',
  age:18
})
let { name , age} = data
data.age = 20 // 响应式
age = 30 // 非响应式

为什么将可观察对象中的属性解构出来后,变成不再可观察了呢?因为通过reactive方法创建的可观察对象,内部的属性本身并不是可观察的,而是通过Proxy代理实现的读写观察,如果将这些属性解构,这些属性就不再通过原对象的代理来访问了,就无法再进行观察。

Composition API提供了一种方法来解决此机制带来的问题,那就是toRefs,它可以将reactive创建的可观察对象,转换成可观察的ref对象

import {reactive, toRefs} from "vue"
const data =reactive({
  name:'lisa',
  age:18
})
let { name , age} = toRefs(data)
data.age = 20 // 响应式
age = 30 // 响应式

在模板中使用reactive生成的可观察对象的时候是这样的:

<template>
  <div>{{obj.name}}</div>
</template>
<script>
import { reactive } from "vue"
export default{
  setup(){
    const data = {
      name :"lisa"
    }
    const obj = reactive(data)
    return {
      obj
    }
  }
} 
</script>

当使用了toRefs的时候在模板中只需要使用name即可

<template>
  <div>{{name}}</div>
</template>
<script>
import { reactive, toRefs } from "vue"
export default{
  setup(){
    const data = {
      name :"lisa"
    }
    const obj = reactive(data)
    return {
      ...toRefs(obj)
    }
  }
} 
</script>

Composition API提供的computed方法就相当于2.x版本中的计算属性。使用如下:

import { ref, computed } from "vue"
const count = ref(0)
const doubleCount = computed(()=>{
  return count.value*2
})

Composition API提供的watch方法相当于就是2.x的观察属性。使用如下:

import { ref, watch } from "vue"
const count = ref(0)
const num = ref(1)
watch(() => { return count.value }, (newcount) => {
  console.log('count变啦', newcount)
})

watch方法接收两个参数,第一个参数是一个函数,第二个参数也是个函数,第一个参数函数返回值表示要监听哪个数据,第二个参数函数,表示监听成功后的逻辑,该函数的第一个参数就是监听到目标数据变化后的值。
同时watch可以监听多个数据。

watch(
  [() => count.value, () => num.value],
  ([count, num], [oldCount, oldNum]) => { // watch 同时观察count和num两个值
    console.log(`count:${count},num:${num} oldCount:${oldCount},oldNum:${oldNum}`)
  })

在Vue2.x版本中频繁出现的this,在Vue3.0中也消失了,取而代之的是Composition API提供的getCurrentInstance方法,用来获取当前组件实例,然后通过ctx获取当前上下文。

import {getCurrentInstance} from "vue"
export default{
  setup(){
    const {ctx} =  getCurrentInstance()
    console.log(ctx)
  }
}

大概是这么个东西


ctx

可以和Vue2.x中this输出对比一下。还是有不小的变动,但常用api都没有发生变化。比如切换路由

const pushRoute = () => { // 编程导航
  ctx.$router.push({
    path: '/about'
  })
}

整体的Options API,到Composition API,大致就是之前很多挂载在Vue原型上的东西,现在都独立成一个方法然后去引用使用。之前组件中的this容易把人绕迷糊,如果采用Composition API就会好很多。之前Vue组件中强制data写在哪里,methods写在哪里,computed写在哪里,而在Vue3.0中这种规定被打破,开发者可以比较自由的组织自己的代码,两者都有自己的好处与弊端。详见可以参考文章https://juejin.im/post/5eb17a0fe51d454dd60cfe0f

最后看一下Vue3.0中的生命周期,生命周期也是有所改动,钩子函数名称均发生变化。beforeCreate,created生命周期在setup方法中自动执行,其余生命周期钩子函数都是从Vue中引入使用(注意在setup方法中使用)

import {
  ref,
  onBeforeMount,
  onMounted,
  onBeforeUpdate,
  onUpdated,
  onBeforeUnmount,
  onUnmounted,
  onErrorCaptured,
  onRenderTracked,
  onRenderTriggered
} from 'vue'
export default {
  setup (props, context) {
    // console.log(props.msg, context)
    const a = ref(0)
    const setA = () => {
      return a.value++
    }
    // 相当于 beforeMount
    onBeforeMount(() => {
      console.log('onBeforeMount')
    })
    // 相当于 mounted
    onMounted(() => {
      console.log('onMounted')
    })
    // 相当于 beforeUpdate
    onBeforeUpdate(() => {
      console.log('onBeforeUpdate')
    })
    // 相当于 updated
    onUpdated(() => {
      console.log('onUpdated')
    })
    // 相当于 beforeDestroy
    onBeforeUnmount(() => {
      console.log('onBeforeUnmount')
    })
    // 相当于 destroyed
    onUnmounted(() => {
      console.log('onUnmounted')
    })
    onErrorCaptured(() => { // 错误监控 参考文章 https://zhuanlan.zhihu.com/p/37404624
      console.log('onErrorCaptured')
    })
    onRenderTracked(() => { // 已渲染
      console.log('onRenderTracked')
    })
    onRenderTriggered(() => { // 当组件更新时会首先触发此生命周期钩子 onRenderTriggered->onRenderTracked->onBeforeUpdate->onUpdated
      console.log('onRenderTriggered')
    })
    return {
      a,
      setA
    }
  },
  name: 'HelloWorld',
  props: {
    msg: String
  }
}

onRenderTracked生命周期钩子函数表示组件已渲染。组件首次渲染经历过程为onRenderTracked->onBeforeMount->onMounted
onErrorCaptured(err,vm,info)生命周期钩子表示捕获子孙组件中的发生错误时的异常。err:错误对象 vm:发生错误的vuez组件实例 info:Vue特定错误信息,比如发生错误的生命周期
onRenderTriggered组件更新时会触发此钩子函数。触发生命周期钩子函数过程为onRenderTriggered->onRenderTracked->onBeforeUpdate->onUpdated

在Vue3.0中由于外界声音反响比较大的原因,尤大以及团队考虑在3.0版本中可以持续使用2.x的东西,比如可以同时写mounted和onMounted两个生命周期,但是不建议这样做,如果使用Vue3.0那么就踏踏实实用3.0的东西去写。如果使用2.x版本的话,可以引用一些方法等,按照需要一点点向3.0慢慢过渡。总之,任何一个框架都是需要更新的,更新肯定会有变化,那么就慢慢学吧。

我的练习源码在github上里面有我写的一些注释,有兴趣的也可以看一看https://github.com/Mstian/Vue3.0-test
文中如有错误,还望不吝指出,谢谢。

参考文章:
vue 3.0 初体验 (项目搭建)
简明扼要聊聊 Vue3.0 的 Composition API 是啥东东!
VUE 3.0 学习探索入门系列 - Vue3.x 生命周期 和 Composition API 核心语法理解(6)
聊聊vue3.0新特性:compositon api 用法和注意事项
Vue源码系列(二):错误处理

偶然发现一些比较不错的资料:
https://www.yuque.com/vueconf/2019