基于Vue cli的Vue3.0初體驗(yàn)

前言

Vue3.0的步伐越來(lái)越近了,是時(shí)候了解起來(lái)了,雖然嘴上還喊學(xué)不動(dòng)了,但是,身體還得誠(chéng)實(shí)起來(lái),接著學(xué)。。。
通過(guò)各種博客資料,還有前段時(shí)間尤雨溪大佬的直播Vue3相對(duì)Vue2的比較大的變化有以下幾種:

  • 使用 TypeScript
  • 放棄 class 采用 function-based API
  • option API => Composition API
  • 重構(gòu) complier
  • 重構(gòu) virtual DOM
  • 新的響應(yīng)式機(jī)制

使用ts的話就是拋棄了谷歌的flow選擇擁抱微軟的ts。從這個(gè)情況也看得出來(lái),不會(huì)ts的該學(xué)起來(lái)了,比如我。。。

放棄class采用function-based API據(jù)說(shuō)也是為了更好支持ts,為了更靈活的邏輯復(fù)用能力,代碼更容易壓縮等...

option Api到Composition Api應(yīng)該是對(duì)我們?nèi)?xiě)代碼影響最大的一部分,稍后可以看代碼體驗(yàn)一下。

重構(gòu)compiler與virtual DOM使Vue變得更快,也是Vue越來(lái)越優(yōu)秀的原因。

新的響應(yīng)式機(jī)制采用了ES6的ProxyApi,拋棄了之前的Object.defineProperty()比較直觀的解決的是Vue2中這兩點(diǎn)問(wèn)題:

  • 關(guān)于對(duì)象:Vue 無(wú)法檢測(cè) property 的添加或移除。由于 Vue 會(huì)在初始化實(shí)例時(shí)對(duì) property 執(zhí)行 getter/setter 轉(zhuǎn)化,所以 property 必須在 data 對(duì)象上存在才能讓 Vue 將它轉(zhuǎn)換為響應(yīng)式的。

  • 關(guān)于數(shù)組:Vue 不能檢測(cè)以下數(shù)組的變動(dòng):

    1. 當(dāng)你利用索引直接設(shè)置一個(gè)數(shù)組項(xiàng)時(shí),例如:vm.items[indexOfItem] = newValue
    2. 當(dāng)你修改數(shù)組的長(zhǎng)度時(shí),例如:vm.items.length = newLength

在官網(wǎng)深入響應(yīng)式原理一章有較詳細(xì)闡述,針對(duì)以上兩種情況解決方法,官網(wǎng)也有給出答案,那就是使用set方法。

Proxy可以完美的解決該問(wèn)題,當(dāng)然好處應(yīng)該不止這些,剩下的慢慢探究吧,Proxy也有缺點(diǎn),那就是兼容性問(wèn)題,有一些瀏覽器不支持,而且無(wú)完全polyfill,瀏覽器支持程度可以查看https://caniuse.com/#search=Proxy

簡(jiǎn)單了解Proxy

Vue核心就是響應(yīng)式數(shù)據(jù),Vue3.0中的響應(yīng)式采用了Proxy那就簡(jiǎn)單看看Proxy是怎么個(gè)樣子的呢。

Proxy 對(duì)象用于定義基本操作的自定義行為(如屬性查找、賦值、枚舉、函數(shù)調(diào)用等)。。。
from MDN,學(xué)習(xí)一個(gè)新的Api官方文檔還是要讀一下哈,雖然讀不懂這么深?yuàn)W的描述。。。

語(yǔ)法:

const p = new Proxy(target, handler)

參數(shù)target表示要使用Proxy包裝的對(duì)象(可以是任何類型的對(duì)象,包括原生數(shù)組,函數(shù),甚至另一個(gè)代理)

參數(shù)handler是一個(gè)通常以函數(shù)作為屬性的對(duì)象,各屬性中的函數(shù)分別定義了在執(zhí)行各種操作時(shí)代理 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是我們要代理的目標(biāo)對(duì)象,get,set方法是參數(shù)handler的兩個(gè)屬性,具體如下:

handler.get()接收三個(gè)參數(shù),第一個(gè)參數(shù)target為代理的目標(biāo)對(duì)象,第二個(gè)參數(shù)prop是代理的目標(biāo)對(duì)象的屬性,第三個(gè)參數(shù)是Proxy或者繼承Proxy的對(duì)象,通常是proxy本身。

handler.set()接收四個(gè)參數(shù),其中三個(gè)參數(shù)都與get方法相同,唯獨(dú)多出來(lái)一個(gè)value表示新的屬性值。

上述代碼表示當(dāng)訪問(wèn)proxy的屬性時(shí),進(jìn)行攔截判斷,該屬性是否是目標(biāo)對(duì)象的屬性,如果是那么就將其值返回出來(lái),否則就返回0。

當(dāng)對(duì)proxy上的屬性進(jìn)行重寫(xiě)時(shí),將重寫(xiě)的該屬性賦值為666。

注意:此時(shí)對(duì)數(shù)據(jù)的劫持,只是劫持了代理對(duì)象proxy,而跟原對(duì)象obj沒(méi)有任何關(guān)系,對(duì)obj進(jìn)行操作,也不會(huì)監(jiān)聽(tīng)到。

proxy實(shí)現(xiàn)一個(gè)簡(jiǎn)易版的數(shù)據(jù)響應(yīng):

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

let obj = { // 源數(shù)據(jù)
  text:'hello world'
}

let proxy = new Proxy(obj, {
  set: function(target, prop, value){ // input事件觸發(fā)進(jìn)行劫持,觸發(fā)update方法
    target[prop] = value
    update(value)
  }
})

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

input.addEventListener('input',function(e){ // 監(jiān)聽(tīng)input數(shù)據(jù)變化,并修改proxy的值
  proxy.text = e.target.value
})

proxy.text = obj.text // 初始化源數(shù)據(jù)

使用Vue CLI體驗(yàn)Vue3.0

第一步,安裝vue-cli

npm install -g @vue/cli

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

vue -V
@vue/cli 4.4.4

如果cli已安裝需要注意其版本應(yīng)該高于cli4.x。

第二步,初始化vue項(xiàng)目

vue create vue-next-test

輸入命令后,出現(xiàn)命令行交互,跟之前一樣,主要是在初始時(shí)勾選上vue-router,vuex,避免在升級(jí)vue3的過(guò)程中手寫(xiě)初始化代碼,會(huì)自動(dòng)生成初始化代碼。
注意:vue3.0項(xiàng)目目前不能直接創(chuàng)建,需從vue2.x升級(jí)。

vue-cli創(chuàng)建

第三步,升級(jí)成Vue3.0項(xiàng)目
以上只是創(chuàng)建了Vue2.x的項(xiàng)目,需要手動(dòng)升級(jí)成Vue3.0的項(xiàng)目
進(jìn)入vue-next-test文件夾cd vue-next-test
輸以下指令:

vue add vue-next

執(zhí)行上述指令會(huì)自動(dòng)安裝vue-cli-plugin-vue-next插件,該插件會(huì)自動(dòng)完成以下操作

  • 安裝vue3.0beta版依賴
  • 配置webpack去在vue3中編譯.vue文件
  • 自動(dòng)遷移全局api(創(chuàng)建新模板)
  • 升級(jí)安裝Vue Router4.0和Vuex 4.0,如果默認(rèn)為未安裝,則不升級(jí)。
  • 自動(dòng)修改Vue Router 和Vuex模板代碼

升級(jí)完成之后就可以看代碼啦!

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

  • Vuex對(duì)比

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版本采用構(gòu)造函數(shù)構(gòu)建Vue Router實(shí)例,而Vue3.0使用createStore方法來(lái)構(gòu)建Vue實(shí)例,Vuex語(yǔ)法和Api基本沒(méi)有發(fā)生變化。和之前一樣,該怎么樣寫(xiě)state,mutations等還是怎么寫(xiě),該怎么調(diào)還怎么調(diào)。

  • Vue Router對(duì)比

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也是之前采用構(gòu)造函數(shù)形式,3.0采用createRouter方法去創(chuàng)建Vue Router實(shí)例,配置方法都一樣。在路由模式配置上,之前是配置mode option,3.0則是采用vue-router中的createWebHistory方法去創(chuàng)建history屬性,我默認(rèn)選擇的是history模式用的是createWebHistory方法創(chuàng)建history屬性,如果要修改為hash模式則需要使用createWebHashHistory方法來(lái)創(chuàng)建。

總結(jié):總的來(lái)說(shuō),構(gòu)建Vue Router和Vuex的方式變了,但是它們的配置方式都和之前保持一致,可以無(wú)縫銜接使用。

  • Composition API
    在3.0代碼基礎(chǔ)上繼續(xù)往下看,創(chuàng)建一個(gè)新的組件<Test/>,在<Test/>組件中認(rèn)識(shí)一下Composition API
    之前2.x版本是采用了Options API的模式,可以理解為選項(xiàng)式的組件代碼編寫(xiě),Vue官方規(guī)定好的寫(xiě)法,響應(yīng)式數(shù)據(jù),methods,computed,components以及生命周期這些都是規(guī)定好的,需要在哪里寫(xiě),你就得在哪里寫(xiě)。
<script>
  export default {
    data:() => {
      return {}
    },
    methods:{
    },
    computed:{
    },
    component:{
    },
    mounted(){
    }
  }
</script>

Vue3.0采用Composition API的模式,可以理解成組合API,怎么個(gè)組合法呢?就類似于,在組件中實(shí)現(xiàn)的這些東西,響應(yīng)式數(shù)據(jù),生命周期,計(jì)算屬性等,都可以在Vue中獲取對(duì)應(yīng)方法,然后在一個(gè)方法中組合起來(lái)統(tǒng)一對(duì)外輸出。
Composition API提供了以下一些函數(shù),

  • ref
  • reactive
  • toRefs
  • computed
  • watch
  • getCurrentInstance
  • 生命周期hooks
  • ...
    在體驗(yàn)Composition API之前需要認(rèn)識(shí)一個(gè)函數(shù)叫做setup(),這個(gè)函數(shù)的主要功能是Composition API的入口,它在生命周期beforeCreate生命周期執(zhí)行之前被調(diào)用,接收props對(duì)象作為第一個(gè)參數(shù),接收來(lái)的props對(duì)象,可以通過(guò)watch監(jiān)視其變化。接受context對(duì)象作為第二個(gè)參數(shù),這個(gè)對(duì)象包含attrs,slots,emit三個(gè)屬性。多說(shuō)無(wú)益,直接看代碼吧。
import { ref, reactive } from 'vue'
export default {
  setup(props, context){
    const count = ref(0) // 定義響應(yīng)式數(shù)據(jù)count
    const num = ref(1) // 定義響應(yīng)式數(shù)據(jù)num
    const objData = {
      name: 'erha',
      age: '1',
      skill: '拆家'
    }
    const obj = reactive(objData) // 定義響應(yīng)式數(shù)據(jù)obj
    return {
      count,
      num,
      obj
    }
  },
  name:'test'
}

在Vue3.0中創(chuàng)建響應(yīng)式數(shù)據(jù)需要引用ref,reactive這兩個(gè)方法,ref一般用來(lái)創(chuàng)建基本數(shù)據(jù)類型的響應(yīng)式數(shù)據(jù),reactive一般用來(lái)創(chuàng)建引用數(shù)據(jù)類型的響應(yīng)式數(shù)據(jù)。
在模板中使用,跟之前沒(méi)有區(qū)別,需要注意的是,ref屬于將基本類型數(shù)據(jù)包裝成應(yīng)用類型,在模板中正常使用。在方法中訪問(wèn)的時(shí)候需要帶上.value才能訪問(wèn)到。
以下代碼我簡(jiǎn)寫(xiě)了,比如有一個(gè)按鈕點(diǎn)擊會(huì)觸發(fā)一個(gè)方法,該方法是讓count自增,那么應(yīng)該這樣寫(xiě):

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

為什么要這么寫(xiě)呢?是因?yàn)镻roxy的原因,Proxy要進(jìn)行數(shù)據(jù)劫持的時(shí)候需要接收一個(gè)對(duì)象,所以ref就對(duì)基本數(shù)據(jù)類型的數(shù)據(jù)進(jìn)行了包裝,使其可以進(jìn)行響應(yīng)式。在方法中需要使用count.value去操作,而在模板中進(jìn)行了處理,所以可以直接使用count進(jìn)行渲染。

由于Proxy的機(jī)制原因,如果將reactive中的響應(yīng)式數(shù)據(jù)進(jìn)行解構(gòu),那么原先的響應(yīng)式數(shù)據(jù)就變成不可響應(yīng)的了。

import { reactive } from 'vue'
const data =reactive({
  name:'lisa',
  age:18
})
let { name , age} = data
data.age = 20 // 響應(yīng)式
age = 30 // 非響應(yīng)式

為什么將可觀察對(duì)象中的屬性解構(gòu)出來(lái)后,變成不再可觀察了呢?因?yàn)橥ㄟ^(guò)reactive方法創(chuàng)建的可觀察對(duì)象,內(nèi)部的屬性本身并不是可觀察的,而是通過(guò)Proxy代理實(shí)現(xiàn)的讀寫(xiě)觀察,如果將這些屬性解構(gòu),這些屬性就不再通過(guò)原對(duì)象的代理來(lái)訪問(wèn)了,就無(wú)法再進(jìn)行觀察。

Composition API提供了一種方法來(lái)解決此機(jī)制帶來(lái)的問(wèn)題,那就是toRefs,它可以將reactive創(chuàng)建的可觀察對(duì)象,轉(zhuǎn)換成可觀察的ref對(duì)象

import {reactive, toRefs} from "vue"
const data =reactive({
  name:'lisa',
  age:18
})
let { name , age} = toRefs(data)
data.age = 20 // 響應(yīng)式
age = 30 // 響應(yīng)式

在模板中使用reactive生成的可觀察對(duì)象的時(shí)候是這樣的:

<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>

當(dāng)使用了toRefs的時(shí)候在模板中只需要使用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方法就相當(dāng)于2.x版本中的計(jì)算屬性。使用如下:

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

Composition API提供的watch方法相當(dāng)于就是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方法接收兩個(gè)參數(shù),第一個(gè)參數(shù)是一個(gè)函數(shù),第二個(gè)參數(shù)也是個(gè)函數(shù),第一個(gè)參數(shù)函數(shù)返回值表示要監(jiān)聽(tīng)哪個(gè)數(shù)據(jù),第二個(gè)參數(shù)函數(shù),表示監(jiān)聽(tīng)成功后的邏輯,該函數(shù)的第一個(gè)參數(shù)就是監(jiān)聽(tīng)到目標(biāo)數(shù)據(jù)變化后的值。
同時(shí)watch可以監(jiān)聽(tīng)多個(gè)數(shù)據(jù)。

watch(
  [() => count.value, () => num.value],
  ([count, num], [oldCount, oldNum]) => { // watch 同時(shí)觀察count和num兩個(gè)值
    console.log(`count:${count},num:${num} oldCount:${oldCount},oldNum:${oldNum}`)
  })

在Vue2.x版本中頻繁出現(xiàn)的this,在Vue3.0中也消失了,取而代之的是Composition API提供的getCurrentInstance方法,用來(lái)獲取當(dāng)前組件實(shí)例,然后通過(guò)ctx獲取當(dāng)前上下文。

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

大概是這么個(gè)東西


ctx

可以和Vue2.x中this輸出對(duì)比一下。還是有不小的變動(dòng),但常用api都沒(méi)有發(fā)生變化。比如切換路由

const pushRoute = () => { // 編程導(dǎo)航
  ctx.$router.push({
    path: '/about'
  })
}

整體的Options API,到Composition API,大致就是之前很多掛載在Vue原型上的東西,現(xiàn)在都獨(dú)立成一個(gè)方法然后去引用使用。之前組件中的this容易把人繞迷糊,如果采用Composition API就會(huì)好很多。之前Vue組件中強(qiáng)制data寫(xiě)在哪里,methods寫(xiě)在哪里,computed寫(xiě)在哪里,而在Vue3.0中這種規(guī)定被打破,開(kāi)發(fā)者可以比較自由的組織自己的代碼,兩者都有自己的好處與弊端。詳見(jiàn)可以參考文章https://juejin.im/post/5eb17a0fe51d454dd60cfe0f

最后看一下Vue3.0中的生命周期,生命周期也是有所改動(dòng),鉤子函數(shù)名稱均發(fā)生變化。beforeCreate,created生命周期在setup方法中自動(dòng)執(zhí)行,其余生命周期鉤子函數(shù)都是從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++
    }
    // 相當(dāng)于 beforeMount
    onBeforeMount(() => {
      console.log('onBeforeMount')
    })
    // 相當(dāng)于 mounted
    onMounted(() => {
      console.log('onMounted')
    })
    // 相當(dāng)于 beforeUpdate
    onBeforeUpdate(() => {
      console.log('onBeforeUpdate')
    })
    // 相當(dāng)于 updated
    onUpdated(() => {
      console.log('onUpdated')
    })
    // 相當(dāng)于 beforeDestroy
    onBeforeUnmount(() => {
      console.log('onBeforeUnmount')
    })
    // 相當(dāng)于 destroyed
    onUnmounted(() => {
      console.log('onUnmounted')
    })
    onErrorCaptured(() => { // 錯(cuò)誤監(jiān)控 參考文章 https://zhuanlan.zhihu.com/p/37404624
      console.log('onErrorCaptured')
    })
    onRenderTracked(() => { // 已渲染
      console.log('onRenderTracked')
    })
    onRenderTriggered(() => { // 當(dāng)組件更新時(shí)會(huì)首先觸發(fā)此生命周期鉤子 onRenderTriggered->onRenderTracked->onBeforeUpdate->onUpdated
      console.log('onRenderTriggered')
    })
    return {
      a,
      setA
    }
  },
  name: 'HelloWorld',
  props: {
    msg: String
  }
}

onRenderTracked生命周期鉤子函數(shù)表示組件已渲染。組件首次渲染經(jīng)歷過(guò)程為onRenderTracked->onBeforeMount->onMounted
onErrorCaptured(err,vm,info)生命周期鉤子表示捕獲子孫組件中的發(fā)生錯(cuò)誤時(shí)的異常。err:錯(cuò)誤對(duì)象 vm:發(fā)生錯(cuò)誤的vuez組件實(shí)例 info:Vue特定錯(cuò)誤信息,比如發(fā)生錯(cuò)誤的生命周期
onRenderTriggered組件更新時(shí)會(huì)觸發(fā)此鉤子函數(shù)。觸發(fā)生命周期鉤子函數(shù)過(guò)程為onRenderTriggered->onRenderTracked->onBeforeUpdate->onUpdated

在Vue3.0中由于外界聲音反響比較大的原因,尤大以及團(tuán)隊(duì)考慮在3.0版本中可以持續(xù)使用2.x的東西,比如可以同時(shí)寫(xiě)mounted和onMounted兩個(gè)生命周期,但是不建議這樣做,如果使用Vue3.0那么就踏踏實(shí)實(shí)用3.0的東西去寫(xiě)。如果使用2.x版本的話,可以引用一些方法等,按照需要一點(diǎn)點(diǎn)向3.0慢慢過(guò)渡??傊魏我粋€(gè)框架都是需要更新的,更新肯定會(huì)有變化,那么就慢慢學(xué)吧。

我的練習(xí)源碼在github上里面有我寫(xiě)的一些注釋,有興趣的也可以看一看https://github.com/Mstian/Vue3.0-test
文中如有錯(cuò)誤,還望不吝指出,謝謝。

參考文章:
vue 3.0 初體驗(yàn) (項(xiàng)目搭建)
簡(jiǎn)明扼要聊聊 Vue3.0 的 Composition API 是啥東東!
VUE 3.0 學(xué)習(xí)探索入門(mén)系列 - Vue3.x 生命周期 和 Composition API 核心語(yǔ)法理解(6)
聊聊vue3.0新特性:compositon api 用法和注意事項(xiàng)
Vue源碼系列(二):錯(cuò)誤處理

偶然發(fā)現(xiàn)一些比較不錯(cuò)的資料:
https://www.yuque.com/vueconf/2019

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容