1. 程式人生 > 其它 >② 初識vue3.0:新特性講解

② 初識vue3.0:新特性講解

1 vue3 新特性巡禮

1.1 效能提升

  • 打包減小

  • 初次渲染加快、更新加快

  • 記憶體使用減少

得益於重寫虛擬 DOM 的實現和 Tree shanking 的優化

1.2 Composition API

  • ref 和 reactive

  • computed 和 watch

  • 新的生命週期函式

  • 自定義函式 -- Hooks 函式

1.3 其他新增特性

  • Teleport -- 瞬移元件的位置

  • Suspense -- 非同步載入元件的新福音

  • 全域性 API 的優化和修改

  • 更多試驗性特性

1.4 更好的 ts 支援

2 為什麼會有 vue3

  • vue2 遇到的難題

1. 隨著功能的增長,複雜元件的程式碼變得難以維護

2. 隨著複雜度的上升,帶來的問題

mixin 的解決方案
const filterMixin = {
  data() {
    return {}
  },
  methods: {}
}
const paginationMixin = {
  data() {
    return {}
  },
  methods: {}
}
export default {
  mixin: [ filterMixin, paginationMixin ]
}
mixin 的缺點
  1. 命名衝突

  2. 不清楚暴露出來變數的作用

  3. 重用到其他元件時會出現問題

3. vue2 對於 ts 的支援非常有限

3 vue3 - ref 的妙用

  • setup

  • ref:處理基本型別的資料

  • computed

import { ref, computed } from 'vue'
export default {
  name: 'App',
  setup() {
    const count = ref(0)
    const double = computed(() => {
      return count.value * 2
    })
    const increase = () => {
      count.value++
    }
    return {
      count, double, increase
    }
  }
}

4 更近一步 - reactive

  • reactive:處理複雜型別的資料

  • reactive + toRefs(將物件屬性轉化為響應式屬性)

使用 toRefs 保證 reactive 物件屬性保持響應性

import { computed, reactive, toRefs } from 'vue'
interface DataProps {
  count: number,
  double: number,
  increase: () => void
}
export default {
  name: 'App',
  setup() {
    const data: DataProps = reactive({
      count: 0,
      increase: () => { data.count++ },
      double: computed(() => data.count * 2)
    }) 
    // 只有 data 是響應式的 其屬性非響應式
    const refData = toRefs(data)
    return {
      ...refData
    }
  }
}

5 vue3 響應式物件的新花樣

響應式原理

  1. vue2
Object.definedProperty(data, 'count', {
  get() {},
  set() {}
})
  1. vue3
new Proxy(data, {
  get(key) {},
  set(key, value) {}
})
  1. vue3應用
import { computed, reactive, toRefs } from 'vue'
interface DataProps {
  numbers: number[];
  person: { name?: string };
}
export default {
  name: 'App',
  setup() {
    const data: DataProps = reactive({
      numbers: [0, 1, 2],
      person: {}
    }) 
    data.numbers[0] = 5
    data.person.name = 'viking'
    const refData = toRefs(data)
    return {
      ...refData
    }
  }
}

6 老瓶新酒 - 生命週期

在 setup 中使用的 hook 名稱和原來生命週期的對應關係

  • beforeCreate -> use setup()

  • created -> use setup()

  • beforeMount -> onBeforeMount

  • mounted -> onMounted

  • beforeUpdate -> onBeforeUpdate

  • updated -> onUpdated

  • beforeUnmount -> onBeforeUnmount

  • unmounted -> onUnmounted

  • errorCaptured -> onErrorCaptured

  • renderTracked -> onRenderTracked 每次渲染後重新收集響應式依賴時執行

  • renderTriggered -> onRenderTriggered 每次觸發頁面重新渲染時執行

import { onMounted, onUpdated, onRenderTriggered, onRenderTracked } from 'vue'
export default {
  name: 'App',
  setup() {
    onMounted(() => {
      console.log('mounted')
    })
    onUpdated(() => {
      console.log('updated')
    })
    onRenderTriggered(() => {
      console.log('renderTriggered') // 點選按鈕時觸發
    })
    onRenderTracked(() => {
      console.log('renderTracked') // 頁面重新整理時觸發
    })
  }
}

7 偵測變化 - watch

  • 一般用法
setup() {
  const greetings = ref('')
  const updateGreeting = () => {
    greetings.value += 'Hello!'
  }
  watch(greetings, (newVal, oldVal) => {
    console.log(newVal, oldVal);
    document.title = 'updated '
  })
  return {
    updateGreeting,
  }
}
  • 偵聽 reactive 方法下的資料 -- getter 寫法 -- 使用箭頭函式
setup() {
  const data: DataProps = reactive({
    count: 0,
  })
  const greetings = ref('')
  const updateGreeting = () => {
    greetings.value += 'Hello!'
  }
  watch([greetings, () => data.count], (newVal, oldVal) => {
    console.log(newVal, oldVal);
    document.title = 'updated ' + greetings.value + data.count
  })
  // 只有 data 是響應式的 其屬性非響應式
  const refData = toRefs(data)
  return {
    updateGreeting,
    ...refData
  }
}

8 模組化開發 -- 滑鼠追蹤器

hooks > useMousePosition.ts

import { reactive, toRefs, onMounted, onUnmounted } from 'vue'
function useMousePosition() {
  const data = reactive({
    x: 0,
    y: 0
  })
  const updateMouse = (e: MouseEvent) => {
    data.x = e.pageX
    data.y = e.pageY
  }
  onMounted(() => {
    document.addEventListener('click', updateMouse)
  })
  onUnmounted(() => {
    document.removeEventListener('click', updateMouse)
  })
  const refData = toRefs(data)
  return { ...refData }
}
export default useMousePosition

App.vue

import useMousePosition from './hooks/useMousePosition'
export default {
  setup() {
    const { x, y } = useMousePosition()
    return {
      x, y,
    }
  }
}

優點

  1. 可以清楚地知道 x y 的來源,這兩個引數是幹什麼的

  2. 可以給 x y 設定任何別名,避免了命名衝突的風險

  3. 這段邏輯可脫離元件存在,只有邏輯程式碼不需要模板

9 模組化難度上升 - useURLLoader

hooks > useURLLoader.ts

import { ref } from 'vue'
import axios from 'axios'

function useURLLoader(url: string) {
  const result = ref(null)
  const loading = ref(true)
  const loaded = ref(false)
  const error = ref(null)

  axios.get(url).then(rawData => {
    loading.value = false
    loaded.value = true
    result.value = rawData.data
  }).catch(e => {
    error.value = e
    loading.value = false
  })
  return {
    result, loading, loaded, error
  }
}

export default useURLLoader

App.vue

import useURLLoader from './hooks/useURLLoader'
export default {
  setup() {
    const { result, loading, loaded } = useURLLoader('https://dog.ceo/api/breeds/image/random')
    return {
      result, loading, loaded
    }
  }
}

10 模組化結合 typescript - 泛型改造

hooks > useURLLoader.ts

function useURLLoader<T>(url: string) {
  const result = ref<T | null>(null)
  // ...
}
export default useURLLoader

App.vue

  • 展示狗狗圖片
import useURLLoader from './hooks/useURLLoader'

interface DogResult {
  message: string;
  status: string;
}
export default {
  name: 'App',
  setup() {
    const { result, loading, loaded } = useURLLoader<DogResult>('https://dog.ceo/api/breeds/image/random')
    watch(result, () => {
      if(result.value) {
        console.log(result.value.message);
      }
    })
    return {
      result, loading, loaded
    }
  }
}
  • 展示貓貓圖片
import useURLLoader from './hooks/useURLLoader'

interface CatResult {
  id: string;
  url: string;
  width: number;
  height: number; 
}
export default {
  name: 'App',
  setup() {
    const { result, loading, loaded } = useURLLoader<CatResult[]>('https://api.thecatapi.com/v1/images/search?limit=1')
    watch(result, () => {
      if(result.value) {
        console.log(result.value[0].url);
      }
    })
    return {
      result, loading, loaded
    }
  }
}

11 Typescript 對 vue3 的加持

使用 defineComponent 包裹元件 + setup 函式引數

  • components > HelloWorld
import { defineComponent } from 'vue';

export default defineComponent({
  name: 'HelloWorld',
  props: {
    msg: {
      required: true,
      type: String
    }
  },
  setup(props, context) {
    console.log(props.msg)
    const { attrs, slots, emit } = context
  }
});

12 元件 Teleport - 瞬間移動

  • Teleport 上有一個 to 的屬性,它接受一個 css query selector 作為引數,代表要把這個元件渲染到哪個 dom 元素中

12.1 原始 modal 元件

1. components > Modal.vue

<teleport to="#modal">
  <div id="center">
    <h1>this is a modal</h1>
  </div>
</teleport>

2. App.vue

<!-- ... -->
  <Modal />
<!-- ... -->
<script lang="ts">
import Modal from './components/Modal.vue'
export default {
  components: { Modal }
  // ...
}
</script>

3. public > index.html

<div id="app"></div>
<div id="modal"></div>

4. 渲染結果

12.2 實現 modal 的開啟關閉

  • 控制組件的顯示與否 -- isOpen

  • 自定義 content -- slot

  • 關閉 modal -- 使用 emit 觸發

1. components > Modal.vue

  1. 文件化 emits-- 一目瞭然要觸發的事件

  2. 支援執行時檢驗

  3. 支援 自動補全

<teleport to="#modal">
 <div id="center" v-if="isOpen">
   <h1>
     <slot>
     this is a modal
     </slot>  
   </h1>
   <button @click="buttonClick">Close</button>
 </div>
</teleport>
export default defineComponent({
  props: {
    isOpen: Boolean
  },
  // 更明確的顯示元件的自定義事件
  emits: {
    'close-modal': null
  },
  setup(props, context) {
    const buttonClick = () => {
      context.emit('close-modal')
    }
    return {
      buttonClick
    }
  }
})

2. App.vue

<Modal :isOpen="modalIsOpen" @close-modal="onModalClose">
  My Modal !!!
</Modal>
<button @click="openModal">open Modal</button>
setup() {
  const modalIsOpen = ref(false)
  const openModal = () => {
    modalIsOpen.value = true
  }
  const onModalClose = () => {
    modalIsOpen.value = false
  }
  return {
    modalIsOpen, openModal, onModalClose
  }
}

13 元件 Suspense - 非同步請求好幫手

  • 元件使用 Suspense,在 setup 中需要返回一個 Promise

13.1 簡單用法

1. components > AsyncShow.vue

<template>
  <h1>{{ result }}</h1>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
  setup() {
    return new Promise ((resolve) => {
      setTimeout(() => {
        return resolve({
          result: 42
        })
      }, 3000);
    })
  }
})
</script>

2. App.vue

<Suspense>
  <template #default>
    <async-show />
  </template>
  <template #fallback>
    <h1>Loading !...</h1>
  </template>
</Suspense>
import AsyncShow from './components/AsyncShow.vue'
export default {
  components: { AsyncShow }
}

13.2 使用 async await

1. components > DogShow.vue

<template>
  <img :src="result && result.message" />
</template>

<script lang="ts">
import axios from 'axios'
import { defineComponent } from 'vue'
export default defineComponent({
  async setup() {
    const rawData = await axios.get('https://dog.ceo/api/breeds/image/random')
    return {
      result: rawData.data
    }
  }
})
</script>

2. App.vue

<Suspense>
  <template #default>
    <div>
      <async-show />
      <dog-show />
    </div>
  </template>
  <template #fallback>
    <h1>Loading !...</h1>
  </template>
</Suspense>
import AsyncShow from './components/AsyncShow.vue'
import DogShow from './components/DogShow.vue'
export default {
  components: { AsyncShow, DogShow }
}

13.3 Suspense 中的錯誤捕獲

import { onErrorCaptured } from 'vue'
setup() {
  const error = ref(null)
  onErrorCaptured((e: any) => {
    error.value = e
    return true
  })
  return {
    error
  }
}

14 Provide / Inject

14.1 父子元件傳值 -- prop

14.2 解決多層級傳值 -- Provide/Inject

  • Provide/Inject:提供了一種在元件之間共享此類值的方式,而不必通過元件樹的每個層級顯式地傳遞 props

目的是為共享那些被認為對於一個元件樹而言是“全域性”的資料

1. provide -- 提供

export default {
  provide: {
    message: 'hello!'
  }
}
  • 應用級提供
import { createApp } from 'vue'
const app = createApp({})
app.provide('message', 'hello!')

2. inject -- 注入

export default {
  inject: ['message'],
  data() {
    return {
      fullMessage: this.message
    }
  }
}
  • 注入別名
export default {
  inject: {
    localMessage: {
      from: 'message'
    }
  }
}
  • 注入預設值
export default {
  inject: {
    message: {
      from: 'message',
      default: 'default value'
    },
    user: {
      default: () => ({ name: 'John' })
    }
  }
}

3. 使用響應性

  • 使用 computed() 函式提供一個計算屬性 -- 使注入響應性地連結到提供者
import { computed } from 'vue'

export default {
  data() {
    return {
      message: 'hello!'
    }
  },
  provide() {
    return {
      // explicitly provide a computed property
      message: computed(() => this.message)
    }
  }
}

15 全域性 API 修改

15.1 入口檔案

1. vue2 的全域性配置

import Vue from 'vue'
import App from './App.vue'
Vue.config.ignoredElements = [/^app-/]
Vue.use(/* ... */)
Vue.mixin(/* ... */)
Vue.component(/* ... */)
Vue.directive(/* ... */)

Vue.prototype.customProperty = () => {}

new Vue({
  render: h => h(App)
}).$mount('#app')
不足
  1. 單元測試中,全域性配置容易汙染全域性環境
  2. 在不同 App 中,難以共享一份有不同配置的 Vue 物件

2. vue3 的全域性配置

import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)
// app 就是一個 App 的例項,設定任何的配置是在不同的 app 例項上面的,不會像vue2 一樣發生任何的衝突

app.config.isCustomElement = tag => tag.startsWith('app-')
app.use(/* ... */)
app.mixin(/* ... */)
app.component(/* ... */)
app.directive(/* ... */)

app.config.globalProperties.customProperty = () => {}

// 當配置結束以後,我們再把 App 使用 mount 方法掛載到固定的 DOM 的節點上。
app.mount(App, '#app')

15.2 全域性配置 Vue.config -> app.config

15.3 全域性註冊類 API

  • Vue.component -> app.component
  • Vue.directive -> app.directive

15.4 行為擴充套件類

  • Vue.mixin -> app.mixin
  • Vue.use -> app.use

15.5 Global API Treeshaking

webpack-Treeshaking

Tree Shaking -- 用於描述移除 js 上下文中的未引用程式碼

1. vue2

import Vue from 'vue'
Vue.nextTick(() => {})
const obj = Vue.observable({})

2. vue3

import Vue, { nextTick, observable } from 'vue'
Vue.nextTick // undefined
nextTick(() => {})
const obj = observable({})