② 初识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({})
posted on 2022-05-27 17:36  pleaseAnswer  阅读(73)  评论(0编辑  收藏  举报