初识Vue3

196 阅读4分钟

Vue3 解决的难题

  • Vue2 中随着功能的增长,复杂组件的代码变得难以维护。

    • 代码分散
    • 逻辑分开,易读性较差
  • Vue2 Mixin 的缺点(vue2中 Mixin 用于解决复杂组件的维护,但是依然有很大的缺点)

    • 命名冲突
    • 不清楚暴露出来变量的作用
    • 重用到其他 component 经常会遇到问题
  • Vue2对于typescript的支持非常有限

Vue3 新特性

Ref

<template>
  <h1>{{count}}</h1>
  <h1>{{double}}</h1>
  <button @click="increase">+1</button>
</template>

<script lang="ts">
import { ref, computed } from 'vue'

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

}
</script>

传入类型

const result = ref<T | null>(null)

ref支持以泛型的方式传入类型

指向元素

<template>
  <div ref="root">This is a root element</div>
</template>

<script>
  import { ref, onMounted } from 'vue'

  export default {
    setup() {
      const root = ref(null)

      onMounted(() => {
        // the DOM element will be assigned to the ref after initial render
        console.log(root.value) // <div>This is a root element</div>
      })

      return {
        root
      }
    }
  }
</script>

Reactive

<template>
  <h1>{{count}}</h1>
  <h1>{{double}}</h1>
  <button @click="increase">+1</button>
</template>

<script lang="ts">
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,
      double: computed(() => data.count * 2),
      increase: () => { data.count++ }
    })
    const refData = toRefs(data) // 使用 toRefs 保证 reactive 对象属性保持响应性

    return {
      ...refData
    }
  }

}
</script>

使用ref还是reactive

  • 可以像原来写js时依照编码习惯来选择 ref 还是reactive,类似于:

    const a = 0
    const b = 1
    // or
    const _var = {
        a: 0,
        b: 1
    }
    
  • 规定所有场景都使用reactive,但是要记得使用 toRefs 保证 reactive 对象属性保持响应性

响应式对象的新特性

因为Vue3使用了es6中的Proxy,所以现在可以直接监听数据的新增以及对象与数组的各种操作而不需要像Vue2那样使用$set和被重写的对象与数组操作

import { computed, reactive, toRefs } from 'vue'

interface DataProps {
  numbers: number[];
  person: any;
}

export default {
  name: 'App',
  setup() {

    const data: DataProps = reactive({
      numbers: [0, 1, 2],
      person: {}
    })
    data.numbers[0] = 5 // 可以不适用 $set 而直接改动数据
    data.person.name = 'viking' // 可以直接新增属性
    const refData = toRefs(data) // 使用 toRefs 保证 reactive 对象属性保持响应性

    return {
      ...refData
    }
  }

}

生命周期的新特性

Vue3的生命周期函数,可以按需导入到组件中,且只能在 setup() 函数中使用

import { onMounted, onUpdated, onRenderTracked } from 'vue'

export default {
  name: 'App',
  setup() {
    onMounted(() => {
      console.log('mounted')
    })
    onUpdated(() => {
      console.log('updated')
    })
    onRenderTracked((event) => {
      console.log(event)
    })
  }
}

setup 中使用的 hook 名称和原来生命周期的对应关系

  • beforeMount -> onBeforeMount
  • mounted -> onMounted
  • beforeUpdate -> onBeforeUpdate
  • updated -> onUpdated
  • beforeDestroy -> onBeforeUnmount
  • destroyed -> onUnmounted
  • errorCaptured -> onErrorCaptured

新增的生命周期

这两个事件都带有一个DebuggerEvent,它使我们能够知道是什么导致了Vue实例中的重新渲染,一般可以用于调试

  • renderTracked -> onRenderTracked
  • renderTriggered -> onRenderTriggered

Watch

import { watch } from 'vue'
// watch 简单应用
watch(data, () => {
  document.title = 'updated ' + data.count
})
// watch 的两个参数,代表新的值和旧的值
watch(refData.count, (newValue, oldValue) => {
  console.log('old', oldValue)
  console.log('new', newValue)
  document.title = 'updated ' + data.count
})

// watch 多个值,返回的也是多个值的数组
watch([greetings, data], (newValue, oldValue) => {
  console.log('old', oldValue)
  console.log('new', newValue)
  document.title = 'updated' + greetings.value + data.count
})

// 使用 getter 的写法 watch reactive 对象中的一项
watch([greetings, () => data.count], (newValue, oldValue) => {
  console.log('old', oldValue)
  console.log('new', newValue)
  document.title = 'updated' + greetings.value + data.count
})

自定义Hook

抽离方法(类Mixins)

/App.vue

<template>
  <h1>X:{{x}}, Y:{{y}}</h1>
</template>

<script lang="ts">
import useMousePosition from './hooks/useMousePosition'

interface DataProps {
  count: number;
  double: number;
  increase: () => void;
}

export default {
  name: 'App',
  setup() {
    const {x, y} = useMousePosition()
    return { x, y }
  }
}
</script>

/hooks/useMousePosition.ts

import {reactive, toRefs, onMounted, onUnmounted} from 'vue'

export default function useMousePosition () {

  const position = reactive({
    x: 0,
    y: 0,
    updateMouse(e: MouseEvent) {
      position.x = e.pageX
      position.y = e.pageY
    }
  })

  onMounted(() => {
    document.addEventListener('click', position.updateMouse)
  })
  onUnmounted(() => {
    document.removeEventListener('click', position.updateMouse)
  })

  return { ...toRefs(position) }
}

Mixins强的地方

  • 可以清楚的知道数据x, y的来源
  • 可以给x, y设置别名,避免冲突
  • 与组件无关的逻辑可以脱离组件

使用类型

/App.vue

<template>
  <div>
    <h1>X:{{x}}, Y:{{y}}</h1>
    <h1 v-if="loading">Loading...</h1>
    <img v-if="loaded" :src="result.message">
  </div>
</template>

<script lang="ts">
import { watch } from 'vue'
import useMousePosition from './hooks/useMousePosition'
import useURLLoader from './hooks/useURLLoader'

interface DataProps {
  count: number;
  double: number;
  increase: () => void;
}

interface DogResult {
  message: string;
  status: string;
}

interface CatResult {
  id: string;
  url: string;
  width: number;
  height: number;
}

export default {
  name: 'App',
  setup() {

    const { x, y } = useMousePosition()
    const { result, loading, loaded } = useURLLoader<DogResult>('https://dog.ceo/api/breeds/image/random')
    // const { result, loading, loaded } = useURLLoader<CatResult[]>('https://api.thecatapi.com/v1/images/search?limit=1')

    watch(result, () => {
      console.log(result.value && result.value.message)
    })

    return { x, y, loading, loaded, result }
  }
}
</script>

/hooks/useURLLoader.ts

import { ref, reactive, toRefs } from 'vue'
import axios from 'axios'

export default function useURLLoader<T> (url: string) {

  // 为使用泛型,单独拎出来
  const result = ref<T | null>(null)

  const state = reactive({
    result,
    loading: true,
    loaded: false,
    error: null
  })

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

  return { ...toRefs(state) }
}

因为使用了ts, 使用 result里的东西会提示不存在并报错,于是我们需要使用到泛型

defineComponent & defineAsyncComponent

<script lang="ts">
import { defineComponent } from 'vue'

export default defineComponent({
  name: 'HelloWorld',
  props: {
    msg: String,
  },
  setup (props, context) {
    // props.msg
    // context.emit
  }
})
</script>
  • defineComponent返回带有类型加工的对象,在其中可以使用Vue2(Legacy)的方法也可以使用setup新特性

  • setup中的props可以访问组件传入的属性,并推论类型

  • setup中的context可以访问上下文也就是Vuethis,其中有:attrsslotsemit

  • defineAsyncComponent异步的组件

Teleport

通常我们写一个modal组件是直接嵌套进去的,会受到相应父组件的干扰。在Vue3中提供了teleport组件使得我们可以将组件转移到相应的位置。

/components/Modal.vue

<template>
  <teleport to="#modal"> // to 指向需要传送的位置
    <div id="center">
      <h2>This is a modal</h2>
    </div>
  </teleport>
</template>

<script>
export default {

}
</script>

<style>
#center {
  width: 200px;
  height: 200px;
  border: 2px solid black;
  background: white;
  position: fixed;
  left: 50%;
  top: 50%;
  margin-left: -100px;
  margin-top: -100px;
}
</style>

这里我们使用to将本应在父组件下的元素传送到了modal元素内

suspense

定义一个异步组件,在 setup 返回一个 Promise

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>

在 App 中使用

<Suspense>
  <template #default>
    <async-show />
  </template>
  <template #fallback>
    <h1>Loading !...</h1>
  </template>
</Suspense>

全局api的变动

Vue2入口与Vue3入口改动

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

会遇到的问题

  • 在单元测试中,全局配置非常容易污染全局环境
  • 在不同的app中,共享一分有不同配置的Vue对象变得非常困难

Vue3 新的写法

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

const app = createApp(App)

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

app.mount('#app')

全局配置

  • config.productionTip-> 删除
  • config.ignoredElements -> config.isCustomElement
  • config.keyCodes -> 删除

全局注册类API

  • Vue.component -> app.component

  • Vue.directive -> app.directive `

行为扩展类API

  • Vue.mixin -> app.mixin

  • Vue.use -> app.use

Global API Treeshaking

Vue3采用 name ports形式的api,是为了配合类似于webpack等打包工具减少打包体积。

类似于:摇树将枯死的叶子摇落下来。

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

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

个人理解为:采用模块化形式而不再是使用对象形式