Skip to content

Vue3 相关 ​

Vue3 对 Vue2 有什么优势 ​

  • 性能更好(编译优化、使用proxy等)
  • 体积更小
  • 更好的TS支持
  • 更好的代码组织
  • 更好的逻辑抽离
  • 更多新功能

Vue3 和 Vue2 的生命周期有什么区别 ​

Options API生命周期

  • beforeDestroy改为beforeUnmount
  • destroyed改为umounted
  • 其他沿用vue2生命周期

Composition API生命周期

js
import {
  onBeforeMount,
  onMounted,
  onBeforeUpdate,
  onUpdated,
  onBeforeUnmount,
  onUnmounted,
} from 'vue'

export default {
  name: 'LifeCycles',
  props: {
    msg: String,
  },
  // setup等于 beforeCreate 和 created
  setup() {
    console.log('setup')

    onBeforeMount(() => {
      console.log('onBeforeMount')
    })
    onMounted(() => {
      console.log('onMounted')
    })
    onBeforeUpdate(() => {
      console.log('onBeforeUpdate')
    })
    onUpdated(() => {
      console.log('onUpdated')
    })
    onBeforeUnmount(() => {
      console.log('onBeforeUnmount')
    })
    onUnmounted(() => {
      console.log('onUnmounted')
    })
  },

  // 兼容vue2生命周期 options API和composition API生命周期二选一
  beforeCreate() {
    console.log('beforeCreate')
  },
  created() {
    console.log('created')
  },
  beforeMount() {
    console.log('beforeMount')
  },
  mounted() {
    console.log('mounted')
  },
  beforeUpdate() {
    console.log('beforeUpdate')
  },
  updated() {
    console.log('updated')
  },
  // beforeDestroy 改名
  beforeUnmount() {
    console.log('beforeUnmount')
  },
  // destroyed 改名
  unmounted() {
    console.log('unmounted')
  },
}
import {
  onBeforeMount,
  onMounted,
  onBeforeUpdate,
  onUpdated,
  onBeforeUnmount,
  onUnmounted,
} from 'vue'

export default {
  name: 'LifeCycles',
  props: {
    msg: String,
  },
  // setup等于 beforeCreate 和 created
  setup() {
    console.log('setup')

    onBeforeMount(() => {
      console.log('onBeforeMount')
    })
    onMounted(() => {
      console.log('onMounted')
    })
    onBeforeUpdate(() => {
      console.log('onBeforeUpdate')
    })
    onUpdated(() => {
      console.log('onUpdated')
    })
    onBeforeUnmount(() => {
      console.log('onBeforeUnmount')
    })
    onUnmounted(() => {
      console.log('onUnmounted')
    })
  },

  // 兼容vue2生命周期 options API和composition API生命周期二选一
  beforeCreate() {
    console.log('beforeCreate')
  },
  created() {
    console.log('created')
  },
  beforeMount() {
    console.log('beforeMount')
  },
  mounted() {
    console.log('mounted')
  },
  beforeUpdate() {
    console.log('beforeUpdate')
  },
  updated() {
    console.log('updated')
  },
  // beforeDestroy 改名
  beforeUnmount() {
    console.log('beforeUnmount')
  },
  // destroyed 改名
  unmounted() {
    console.log('unmounted')
  },
}

Vue3 如何实现响应式 ​

  • 回顾vue2的Object.defineProperty
  • 缺点
    • 深度监听对象需要一次性递归
    • 无法监听新增属性、删除属性(Vue.set、Vue.delete)
    • 无法监听原生数组,需要特殊处理
  • 学习proxy语法
  • Vue3中如何使用proxy实现响应式

Proxy 基本使用

js
// const data = {
//     name: 'zhangsan',
//     age: 20,
// }
const data = ['a', 'b', 'c']

const proxyData = new Proxy(data, {
  get(target, key, receiver) {
    // 只处理本身(非原型的)属性
    const ownKeys = Reflect.ownKeys(target)
    if (ownKeys.includes(key)) {
      console.log('get', key) // 监听
    }

    const result = Reflect.get(target, key, receiver)
    return result // 返回结果
  },
  set(target, key, val, receiver) {
    // 重复的数据,不处理
    if (val === target[key]) {
      return true
    }

    const result = Reflect.set(target, key, val, receiver)
    console.log('set', key, val)
    // console.log('result', result) // true
    return result // 是否设置成功
  },
  deleteProperty(target, key) {
    const result = Reflect.deleteProperty(target, key)
    console.log('delete property', key)
    // console.log('result', result) // true
    return result // 是否删除成功
  },
})
// const data = {
//     name: 'zhangsan',
//     age: 20,
// }
const data = ['a', 'b', 'c']

const proxyData = new Proxy(data, {
  get(target, key, receiver) {
    // 只处理本身(非原型的)属性
    const ownKeys = Reflect.ownKeys(target)
    if (ownKeys.includes(key)) {
      console.log('get', key) // 监听
    }

    const result = Reflect.get(target, key, receiver)
    return result // 返回结果
  },
  set(target, key, val, receiver) {
    // 重复的数据,不处理
    if (val === target[key]) {
      return true
    }

    const result = Reflect.set(target, key, val, receiver)
    console.log('set', key, val)
    // console.log('result', result) // true
    return result // 是否设置成功
  },
  deleteProperty(target, key) {
    const result = Reflect.deleteProperty(target, key)
    console.log('delete property', key)
    // console.log('result', result) // true
    return result // 是否删除成功
  },
})

vue3 用 Proxy 实现响应式

  • 深度监听,性能更好(获取到哪一层才触发响应式get,不是一次性递归)
  • 可监听新增/删除属性
  • 可监听数组变化
js
// 创建响应式
function reactive(target = {}) {
  if (typeof target !== 'object' || target == null) {
    // 不是对象或数组,则返回
    return target
  }

  // 代理配置
  const proxyConf = {
    get(target, key, receiver) {
      // 只处理本身(非原型的)属性
      const ownKeys = Reflect.ownKeys(target)
      if (ownKeys.includes(key)) {
        console.log('get', key) // 监听
      }

      const result = Reflect.get(target, key, receiver)

      // 深度监听
      // 性能如何提升的?获取到哪一层才触发响应式get,不是一次性递归
      return reactive(result)
    },
    set(target, key, val, receiver) {
      // 重复的数据,不处理
      if (val === target[key]) {
        return true
      }

      const ownKeys = Reflect.ownKeys(target)
      if (ownKeys.includes(key)) {
        console.log('已有的 key', key)
      } else {
        console.log('新增的 key', key)
      }

      const result = Reflect.set(target, key, val, receiver)
      console.log('set', key, val)
      // console.log('result', result) // true
      return result // 是否设置成功
    },
    deleteProperty(target, key) {
      const result = Reflect.deleteProperty(target, key)
      console.log('delete property', key)
      // console.log('result', result) // true
      return result // 是否删除成功
    },
  }

  // 生成代理对象
  const observed = new Proxy(target, proxyConf)
  return observed
}

// 测试数据
const data = {
  name: 'zhangsan',
  age: 20,
  info: {
    city: 'shenshen',
    a: {
      b: {
        c: {
          d: {
            e: 100,
          },
        },
      },
    },
  },
}

const proxyData = reactive(data)
// 创建响应式
function reactive(target = {}) {
  if (typeof target !== 'object' || target == null) {
    // 不是对象或数组,则返回
    return target
  }

  // 代理配置
  const proxyConf = {
    get(target, key, receiver) {
      // 只处理本身(非原型的)属性
      const ownKeys = Reflect.ownKeys(target)
      if (ownKeys.includes(key)) {
        console.log('get', key) // 监听
      }

      const result = Reflect.get(target, key, receiver)

      // 深度监听
      // 性能如何提升的?获取到哪一层才触发响应式get,不是一次性递归
      return reactive(result)
    },
    set(target, key, val, receiver) {
      // 重复的数据,不处理
      if (val === target[key]) {
        return true
      }

      const ownKeys = Reflect.ownKeys(target)
      if (ownKeys.includes(key)) {
        console.log('已有的 key', key)
      } else {
        console.log('新增的 key', key)
      }

      const result = Reflect.set(target, key, val, receiver)
      console.log('set', key, val)
      // console.log('result', result) // true
      return result // 是否设置成功
    },
    deleteProperty(target, key) {
      const result = Reflect.deleteProperty(target, key)
      console.log('delete property', key)
      // console.log('result', result) // true
      return result // 是否删除成功
    },
  }

  // 生成代理对象
  const observed = new Proxy(target, proxyConf)
  return observed
}

// 测试数据
const data = {
  name: 'zhangsan',
  age: 20,
  info: {
    city: 'shenshen',
    a: {
      b: {
        c: {
          d: {
            e: 100,
          },
        },
      },
    },
  },
}

const proxyData = reactive(data)

如何理解 Composition API 和 Options API ​

  • Composition API 带来了什么
    • 更好的代码组织
    • 更好的逻辑复用
    • 更好的类型推导
  • Composition API 和Options API 如何选择
    • 不建议共用,会引起混乱
    • 小型项目、业务逻辑简单,用Option API成本更小一些
    • 中大型项目、逻辑复杂,用Composition API

Composition API 如何实现逻辑复用 ​

  • 抽离逻辑代码到一个函数
  • 函数命名约定为useXx格式(React Hooks也是)
  • 在setup中引用useXx函数
vue
<template>
  <p>mouse position {{ x }} {{ y }}</p>
</template>

<script>
import { reactive } from 'vue'
import useMousePosition from './useMousePosition'
// import useMousePosition2 from './useMousePosition'

export default {
  name: 'MousePosition',
  setup() {
    const { x, y } = useMousePosition()
    return {
      x,
      y,
    }

    // const state = useMousePosition2()
    // return {
    //     state
    // }
  },
}
</script>

import { reactive, ref, onMounted, onUnmounted } from 'vue' function useMousePosition() { const x =
ref(0) const y = ref(0) function update(e) { x.value = e.pageX y.value = e.pageY } onMounted(() => {
console.log('useMousePosition mounted') window.addEventListener('mousemove', update) })
onUnmounted(() => { console.log('useMousePosition unMounted')
window.removeEventListener('mousemove', update) }) // 合成函数尽量返回ref或toRefs(state) state =
reactive({}) // 这样在使用的时候可以解构但不丢失响应式 return { x, y } } // function
useMousePosition2() { // const state = reactive({ // x: 0, // y: 0 // }) // function update(e) { //
state.x = e.pageX // state.y = e.pageY // } // onMounted(() => { // console.log('useMousePosition
mounted') // window.addEventListener('mousemove', update) // }) // onUnmounted(() => { //
console.log('useMousePosition unMounted') // window.removeEventListener('mousemove', update) // })
// return state // } export default useMousePosition // export default useMousePosition2
<template>
  <p>mouse position {{ x }} {{ y }}</p>
</template>

<script>
import { reactive } from 'vue'
import useMousePosition from './useMousePosition'
// import useMousePosition2 from './useMousePosition'

export default {
  name: 'MousePosition',
  setup() {
    const { x, y } = useMousePosition()
    return {
      x,
      y,
    }

    // const state = useMousePosition2()
    // return {
    //     state
    // }
  },
}
</script>

import { reactive, ref, onMounted, onUnmounted } from 'vue' function useMousePosition() { const x =
ref(0) const y = ref(0) function update(e) { x.value = e.pageX y.value = e.pageY } onMounted(() => {
console.log('useMousePosition mounted') window.addEventListener('mousemove', update) })
onUnmounted(() => { console.log('useMousePosition unMounted')
window.removeEventListener('mousemove', update) }) // 合成函数尽量返回ref或toRefs(state) state =
reactive({}) // 这样在使用的时候可以解构但不丢失响应式 return { x, y } } // function
useMousePosition2() { // const state = reactive({ // x: 0, // y: 0 // }) // function update(e) { //
state.x = e.pageX // state.y = e.pageY // } // onMounted(() => { // console.log('useMousePosition
mounted') // window.addEventListener('mousemove', update) // }) // onUnmounted(() => { //
console.log('useMousePosition unMounted') // window.removeEventListener('mousemove', update) // })
// return state // } export default useMousePosition // export default useMousePosition2

Composition API 和 React Hooks 的对比 ​

  • Composition API的setup(相当于created、beforeCreate的合集)只会调用一次,而React Hooks函数在渲染过程中会被多次调用
  • Composition API无需使用useMemo、useCallback避免子组件重复渲染,因为setup只会调用一次,在setup闭包中缓存了变量
  • Composition API无需顾虑调用顺序,而React Hooks需要保证hooks的顺序一致(比如不能放在循环、判断里面)
  • Composition API的ref、reactive比useState难理解

Vue3 的设计目标是什么?做了哪些优化 ​

1、设计目标

不以解决实际业务痛点的更新都是耍流氓,下面我们来列举一下Vue3之前我们或许会面临的问题

  • 随着功能的增长,复杂组件的代码变得越来越难以维护
  • 缺少一种比较「干净」的在多个组件之间提取和复用逻辑的机制
  • 类型推断不够友好
  • bundle的时间太久了

而 Vue3 经过长达两三年时间的筹备,做了哪些事情?

我们从结果反推

  • 更小
  • 更快
  • TypeScript 支持
  • API 设计一致性
  • 提高自身可维护性
  • 开放更多底层功能

一句话概述,就是更小更快更友好了

更小

  • Vue3移除一些不常用的 API
  • 引入tree-shaking,可以将无用模块“剪辑”,仅打包需要的,使打包的整体体积变小了

更快

主要体现在编译方面:

  • diff算法优化
  • 静态提升
  • 事件监听缓存
  • SSR优化

更友好

vue3在兼顾vue2的options API的同时还推出了composition API,大大增加了代码的逻辑组织和代码复用能力

这里代码简单演示下:

存在一个获取鼠标位置的函数

js
import { toRefs, reactive } from 'vue'
function useMouse() {
  const state = reactive({ x: 0, y: 0 })
  const update = (e) => {
    state.x = e.pageX
    state.y = e.pageY
  }
  onMounted(() => {
    window.addEventListener('mousemove', update)
  })
  onUnmounted(() => {
    window.removeEventListener('mousemove', update)
  })

  return toRefs(state)
}
import { toRefs, reactive } from 'vue'
function useMouse() {
  const state = reactive({ x: 0, y: 0 })
  const update = (e) => {
    state.x = e.pageX
    state.y = e.pageY
  }
  onMounted(() => {
    window.addEventListener('mousemove', update)
  })
  onUnmounted(() => {
    window.removeEventListener('mousemove', update)
  })

  return toRefs(state)
}

我们只需要调用这个函数,即可获取x、y的坐标,完全不用关注实现过程

试想一下,如果很多类似的第三方库,我们只需要调用即可,不必关注实现过程,开发效率大大提高

同时,VUE3是基于typescipt编写的,可以享受到自动的类型定义提示

2、优化方案

vue3从很多层面都做了优化,可以分成三个方面:

  • 源码
  • 性能
  • 语法 API

源码

源码可以从两个层面展开:

  • 源码管理
  • TypeScript

源码管理

vue3整个源码是通过 monorepo的方式维护的,根据功能将不同的模块拆分到packages目录下面不同的子目录中

image-20241105153155628

这样使得模块拆分更细化,职责划分更明确,模块之间的依赖关系也更加明确,开发人员也更容易阅读、理解和更改所有模块源码,提高代码的可维护性

另外一些 package(比如 reactivity 响应式库)是可以独立于 Vue 使用的,这样用户如果只想使用 Vue3的响应式能力,可以单独依赖这个响应式库而不用去依赖整个 Vue

TypeScript

Vue3是基于typeScript编写的,提供了更好的类型检查,能支持复杂的类型推导

性能

vue3是从什么哪些方面对性能进行进一步优化呢?

  • 体积优化
  • 编译优化
  • 数据劫持优化

这里讲述数据劫持:

在vue2中,数据劫持是通过Object.defineProperty,这个 API 有一些缺陷,并不能检测对象属性的添加和删除

js
Object.defineProperty(data, 'a', {
  get() {
    // track
  },
  set() {
    // trigger
  },
})
Object.defineProperty(data, 'a', {
  get() {
    // track
  },
  set() {
    // trigger
  },
})

尽管Vue为了解决这个问题提供了 set和delete实例方法,但是对于用户来说,还是增加了一定的心智负担

同时在面对嵌套层级比较深的情况下,就存在性能问题

js
default {
  data: {
    a: {
      b: {
          c: {
          d: 1
        }
      }
    }
  }
}
default {
  data: {
    a: {
      b: {
          c: {
          d: 1
        }
      }
    }
  }
}

相比之下,vue3是通过proxy监听整个对象,那么对于删除还是监听当然也能监听到

同时Proxy 并不能监听到内部深层次的对象变化,而 Vue3 的处理方式是在getter 中去递归响应式,这样的好处是真正访问到的内部对象才会变成响应式,而不是无脑递归

语法 API

这里当然说的就是composition API,其两大显著的优化:

  • 优化逻辑组织
  • 优化逻辑复用

逻辑组织

一张图,我们可以很直观地感受到 Composition API在逻辑组织方面的优势

img

相同功能的代码编写在一块,而不像options API那样,各个功能的代码混成一块

逻辑复用

在vue2中,我们是通过mixin实现功能混合,如果多个mixin混合,会存在两个非常明显的问题:命名冲突和数据来源不清晰

而通过composition这种形式,可以将一些复用的代码抽离出来作为一个函数,只要的使用的地方直接进行调用即可

同样是上文的获取鼠标位置的例子

js
import { toRefs, reactive, onUnmounted, onMounted } from 'vue'
function useMouse() {
  const state = reactive({ x: 0, y: 0 })
  const update = (e) => {
    state.x = e.pageX
    state.y = e.pageY
  }
  onMounted(() => {
    window.addEventListener('mousemove', update)
  })
  onUnmounted(() => {
    window.removeEventListener('mousemove', update)
  })

  return toRefs(state)
}
import { toRefs, reactive, onUnmounted, onMounted } from 'vue'
function useMouse() {
  const state = reactive({ x: 0, y: 0 })
  const update = (e) => {
    state.x = e.pageX
    state.y = e.pageY
  }
  onMounted(() => {
    window.addEventListener('mousemove', update)
  })
  onUnmounted(() => {
    window.removeEventListener('mousemove', update)
  })

  return toRefs(state)
}

组件使用

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

可以看到,整个数据来源清晰了,即使去编写更多的hook函数,也不会出现命名冲突的问题

Vue3 有了解过吗?能说说跟 vue2 的区别吗? ​

1. 哪些变化

image-20241105153214628

从上图中,我们可以概览Vue3的新特性,如下:

  • 速度更快
  • 体积减少
  • 更易维护
  • 更接近原生
  • 更易使用

1.1 速度更快

vue3`相比`vue2
vue3`相比`vue2
  • 重写了虚拟Dom实现
  • 编译模板的优化
  • 更高效的组件初始化
  • undate性能提高 1.3~2 倍
  • SSR速度提高了 2~3 倍

image-20241105153300325

1.2 体积更小

通过webpack的tree-shaking功能,可以将无用模块“剪辑”,仅打包需要的

能够tree-shaking,有两大好处:

  • 对开发人员,能够对vue实现更多其他的功能,而不必担忧整体体积过大
  • 对使用者,打包出来的包体积变小了

vue可以开发出更多其他的功能,而不必担忧vue打包出来的整体体积过多

image-20241105153312449

1.3 更易维护

compositon Api

  • 可与现有的Options API一起使用
  • 灵活的逻辑组合与复用
  • Vue3模块可以和其他框架搭配使用

img

更好的 Typescript 支持

VUE3是基于typescipt编写的,可以享受到自动的类型定义提示

img

1.4 编译器重写

img

1.5 更接近原生

可以自定义渲染 API

image-20241105153323964

1.6 更易使用

响应式 Api 暴露出来

image-20241105153330575

轻松识别组件重新渲染原因

img

2. Vue3 新增特性

Vue 3 中需要关注的一些新功能包括:

  • framents
  • Teleport
  • composition Api
  • createRenderer

2.1 framents

在 Vue3.x 中,组件现在支持有多个根节点

js
<!-- Layout.vue -->
<template>
  <header>...</header>
  <main v-bind="$attrs">...</main>
  <footer>...</footer>
</template>
<!-- Layout.vue -->
<template>
  <header>...</header>
  <main v-bind="$attrs">...</main>
  <footer>...</footer>
</template>

2.2 Teleport

Teleport 是一种能够将我们的模板移动到 DOM 中 Vue app 之外的其他位置的技术,就有点像哆啦 A 梦的“任意门”

在vue2中,像 modals,toast 等这样的元素,如果我们嵌套在 Vue 的某个组件内部,那么处理嵌套组件的定位、z-index 和样式就会变得很困难

通过Teleport,我们可以在组件的逻辑位置写模板代码,然后在 Vue 应用范围之外渲染它

html
<button @click="showToast" class="btn">打开 toast</button>
<!-- to 属性就是目标位置 -->
<teleport to="#teleport-target">
  <div v-if="visible" class="toast-wrap">
    <div class="toast-msg">我是一个 Toast 文案</div>
  </div>
</teleport>
<button @click="showToast" class="btn">打开 toast</button>
<!-- to 属性就是目标位置 -->
<teleport to="#teleport-target">
  <div v-if="visible" class="toast-wrap">
    <div class="toast-msg">我是一个 Toast 文案</div>
  </div>
</teleport>

2.3 createRenderer

通过createRenderer,我们能够构建自定义渲染器,我们能够将 vue 的开发模型扩展到其他平台

我们可以将其生成在canvas画布上

img

关于createRenderer,我们了解下基本使用,就不展开讲述了

js
import { createRenderer } from '@vue/runtime-core'

const { render, createApp } = createRenderer({
  patchProp,
  insert,
  remove,
  createElement,
  // ...
})

export { render, createApp }

export * from '@vue/runtime-core'
import { createRenderer } from '@vue/runtime-core'

const { render, createApp } = createRenderer({
  patchProp,
  insert,
  remove,
  createElement,
  // ...
})

export { render, createApp }

export * from '@vue/runtime-core'

2.4 composition Api

composition Api,也就是组合式api,通过这种形式,我们能够更加容易维护我们的代码,将相同功能的变量进行一个集中式的管理

img

关于compositon api的使用,这里以下图展开

img

简单使用:

js
export default {
  setup() {
    const count = ref(0)
    const double = computed(() => count.value * 2)
    function increment() {
      count.value++
    }
    onMounted(() => console.log('component mounted!'))
    return {
      count,
      double,
      increment,
    }
  },
}
export default {
  setup() {
    const count = ref(0)
    const double = computed(() => count.value * 2)
    function increment() {
      count.value++
    }
    onMounted(() => console.log('component mounted!'))
    return {
      count,
      double,
      increment,
    }
  },
}

3. 非兼容变更

3.1 Global API

  • 全局 Vue API 已更改为使用应用程序实例
  • 全局和内部 API 已经被重构为可 tree-shakable

3.2 模板指令

  • 组件上 v-model 用法已更改
  • <template v-for>和 非 v-for节点上key用法已更改
  • 在同一元素上使用的 v-if 和 v-for 优先级已更改
  • v-bind="object" 现在排序敏感
  • v-for 中的 ref 不再注册 ref 数组

3.3 组件

  • 只能使用普通函数创建功能组件
  • functional 属性在单文件组件 (SFC)
  • 异步组件现在需要 defineAsyncComponent 方法来创建

3.4 渲染函数

  • 渲染函数API改变

  • $scopedSlots property 已删除,所有插槽都通过 $slots 作为函数暴露

  • 自定义指令 API 已更改为与组件生命周期一致

  • 一些转换

    class
    class

    被重命名了:

    • v-enter -> v-enter-from
    • v-leave -> v-leave-from
  • 组件 watch 选项和实例方法 $watch不再支持点分隔字符串路径,请改用计算函数作为参数

  • 在 Vue 2.x 中,应用根容器的 outerHTML 将替换为根组件模板 (如果根组件没有模板/渲染选项,则最终编译为模板)。VUE3.x 现在使用应用程序容器的 innerHTML。

3.5 其他小改变

  • destroyed 生命周期选项被重命名为 unmounted
  • beforeDestroy 生命周期选项被重命名为 beforeUnmount
  • prop default工厂函数不再有权访问 this 是上下文
  • 自定义指令 API 已更改为与组件生命周期一致
  • data 应始终声明为函数
  • 来自 mixin 的 data 选项现在可简单地合并
  • attribute 强制策略已更改
  • 一些过渡 class 被重命名
  • 组建 watch 选项和实例方法 $watch不再支持以点分隔的字符串路径。请改用计算属性函数作为参数。
  • <template> 没有特殊指令的标记 (v-if/else-if/else、v-for 或 v-slot) 现在被视为普通元素,并将生成原生的 <template> 元素,而不是渲染其内部内容。
  • 在Vue 2.x 中,应用根容器的 outerHTML 将替换为根组件模板 (如果根组件没有模板/渲染选项,则最终编译为模板)。Vue 3.x 现在使用应用容器的 innerHTML,这意味着容器本身不再被视为模板的一部分。

3.6 移除 API

  • keyCode 支持作为 v-on 的修饰符
  • $on,$off和$once 实例方法
  • 过滤filter
  • 内联模板 attribute
  • $destroy 实例方法。用户不应再手动管理单个Vue 组件的生命周期。

你知道哪些 Vue3 新特性? ​

官网列举的最值得注意的新特性:v3-migration.vuejs.org(opens new window)

img

  • Composition API
  • SFC Composition API语法糖
  • Teleport传送门
  • Fragments片段
  • Emits选项
  • 自定义渲染器
  • SFC CSS变量
  • Suspense

以上这些是 api 相关,另外还有很多框架特性也不能落掉

回答范例

  1. api层面Vue3新特性主要包括:Composition API、SFC Composition API语法糖、Teleport传送门、Fragments 片段、Emits选项、自定义渲染器、SFC CSS变量、Suspense
  2. 另外,Vue3.0在框架层面也有很多亮眼的改进:
  • 更快
    • 虚拟DOM重写,diff算法优化
    • 编译器优化:静态提升、patchFlags(静态标记)、事件监听缓存
    • 基于Proxy的响应式系统
    • SSR优化
  • 更小:更好的摇树优化 tree shaking、Vue3移除一些不常用的 API
  • 更友好:vue3在兼顾vue2的options API的同时还推出了composition API,大大增加了代码的逻辑组织和代码复用能力
  • 更容易维护:TypeScript + 模块化
  • 更容易扩展
    • 独立的响应化模块
    • 自定义渲染器

Vue3 速度快的原因 ​

Vue3.0 性能提升体现在哪些方面

  • 代码层面性能优化主要体现在全新响应式API,基于Proxy实现,性能更好(获取到哪一层才触发响应式get,不是像vue2一次性递归监听数据)

  • 编译层面做了更多编译优化处理,比如静态标记 pachFlag(diff算法增加了一个静态标记,只对比有标记的dom元素)、事件增加缓存、静态提升(对不参与更新的元素,会做静态提升,只会被创建一次,之后会在每次渲染时候被不停的复用)等,可以有效跳过大量diff过程;

  • 打包时更好的支持tree-shaking,因此整体体积更小,加载更快

  • ssr渲染以字符串方式渲染

  • proxy响应式:深度监听,性能更好(获取到哪一层才触发响应式get,不是一次性递归)

  • PatchFlag 动态节点做标志

  • HoistStatic 将静态节点的定义,提升到父作用域,缓存起来。多个相邻的静态节点,会被合并起来

  • CacheHandler 事件缓存

  • SSR优化: 静态节点不走vdom逻辑,直接输出字符串

  • Tree-shaking 根据模板的内容动态import不同的内容,不需要就不import

一、编译阶段

试想一下,一个组件结构如下图

html
<template>
  <div id="content">
    <p class="text">静态文本</p>
    <p class="text">静态文本</p>
    <p class="text">{ message }</p>
    <p class="text">静态文本</p>
    ...
    <p class="text">静态文本</p>
  </div>
</template>
<template>
  <div id="content">
    <p class="text">静态文本</p>
    <p class="text">静态文本</p>
    <p class="text">{ message }</p>
    <p class="text">静态文本</p>
    ...
    <p class="text">静态文本</p>
  </div>
</template>

可以看到,组件内部只有一个动态节点,剩余一堆都是静态节点,所以这里很多 diff 和遍历其实都是不需要的,造成性能浪费

因此,Vue3 在编译阶段,做了进一步优化。主要有如下:

  • diff算法优化
  • 静态提升
  • 事件监听缓存
  • SSR优化

1. diff 算法优化

  • Vue 2x 中的虚拟 dom 是进行全量的对比。
  • Vue 3x 中新增了静态标记(PatchFlag):在与上次虚拟结点进行对比的时候,值对比 带有 patch flag 的节点,并且可以通过 flag 的信息得知当前节点要对比的具体内容化

什么是 PatchFlag

  • 模板编译时,动态节点做标记
  • 标记,分为不同类型,如Text、PROPS、CLASS
  • diff算法时,可区分静态节点,以及不同类型的动态节点

image-20241105155153511

vue
<!-- https://vue-next-template-explorer.netlify.app 中打开查看编译结果 -->

<div>
  <span>hello vue3</span>
  <span>{{msg}}</span>
  <span :class="name">poetry</span>
  <span :id="name">poetry</span>
  <span :id="name">{{msg}}</span>
  <span :id="name" :msg="msg">poetry</span>
</div>

// 编译后结果 import { createElementVNode as _createElementVNode, toDisplayString as
_toDisplayString, normalizeClass as _normalizeClass, openBlock as _openBlock, createElementBlock as
_createElementBlock } from "vue" export function render(_ctx, _cache, $props, $setup, $data,
$options) { return (_openBlock(), _createElementBlock("div", null, [ _createElementVNode("span",
null, "hello vue3"), _createElementVNode("span", null, _toDisplayString(_ctx.msg), 1 /* TEXT */), //
文本标记1 _createElementVNode("span", { class: _normalizeClass(_ctx.name) }, "poetry", 2 /* CLASS
*/), // class标记2 _createElementVNode("span", { id: _ctx.name }, "poetry", 8 /* PROPS */, ["id"]),
// 属性props标记8 _createElementVNode("span", { id: _ctx.name }, _toDisplayString(_ctx.msg), 9 /*
TEXT, PROPS */, ["id"]), // 文本和属性组合标记9 _createElementVNode("span", { id: _ctx.name, msg:
_ctx.msg }, "poetry", 8 /* PROPS */, ["id", "msg"]) // 属性组合标记 ])) }
<!-- https://vue-next-template-explorer.netlify.app 中打开查看编译结果 -->

<div>
  <span>hello vue3</span>
  <span>{{msg}}</span>
  <span :class="name">poetry</span>
  <span :id="name">poetry</span>
  <span :id="name">{{msg}}</span>
  <span :id="name" :msg="msg">poetry</span>
</div>

// 编译后结果 import { createElementVNode as _createElementVNode, toDisplayString as
_toDisplayString, normalizeClass as _normalizeClass, openBlock as _openBlock, createElementBlock as
_createElementBlock } from "vue" export function render(_ctx, _cache, $props, $setup, $data,
$options) { return (_openBlock(), _createElementBlock("div", null, [ _createElementVNode("span",
null, "hello vue3"), _createElementVNode("span", null, _toDisplayString(_ctx.msg), 1 /* TEXT */), //
文本标记1 _createElementVNode("span", { class: _normalizeClass(_ctx.name) }, "poetry", 2 /* CLASS
*/), // class标记2 _createElementVNode("span", { id: _ctx.name }, "poetry", 8 /* PROPS */, ["id"]),
// 属性props标记8 _createElementVNode("span", { id: _ctx.name }, _toDisplayString(_ctx.msg), 9 /*
TEXT, PROPS */, ["id"]), // 文本和属性组合标记9 _createElementVNode("span", { id: _ctx.name, msg:
_ctx.msg }, "poetry", 8 /* PROPS */, ["id", "msg"]) // 属性组合标记 ])) }

Vue2.x 的 diff 算法

vue2.x的diff算法叫做全量比较,顾名思义,就是当数据改变的时候,会从头到尾的进行vDom对比,即使有些内容是永恒固定不变的

img

Vue3.0 的 diff 算法

vue3.0的diff算法有个叫静态标记(PatchFlag)的小玩意,啥是静态标记呢?简单点说,就是如果你的内容会变,我会给你一个flag,下次数据更新的时候我直接来对比你,我就不对比那些没有标记的了

img

已经标记静态节点的p标签在diff过程中则不会比较,把性能进一步提高

js
export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (
    _openBlock(),
    _createBlock('div', null, [
      _createVNode('p', null, "'HelloWorld'"),
      _createVNode('p', null, _toDisplayString(_ctx.msg), 1 /* TEXT */),
      //上面这个1就是静态标记
    ])
  )
}
export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (
    _openBlock(),
    _createBlock('div', null, [
      _createVNode('p', null, "'HelloWorld'"),
      _createVNode('p', null, _toDisplayString(_ctx.msg), 1 /* TEXT */),
      //上面这个1就是静态标记
    ])
  )
}

关于静态类型枚举如下

js
TEXT = 1 // 动态文本节点
CLASS=1<<1,1 // 2//动态class
STYLE=1<<2,// 4 //动态style
PROPS=1<<3,// 8 //动态属性,但不包含类名和样式
FULLPR0PS=1<<4,// 16 //具有动态key属性,当key改变时,需要进行完整的diff比较。
HYDRATE_ EVENTS = 1 << 5,// 32 //带有监听事件的节点
STABLE FRAGMENT = 1 << 6, // 64 //一个不会改变子节点顺序的fragment
KEYED_ FRAGMENT = 1 << 7, // 128 //带有key属性的fragment 或部分子字节有key
UNKEYED FRAGMENT = 1<< 8, // 256 //子节点没有key 的fragment
NEED PATCH = 1 << 9, // 512 //一个节点只会进行非props比较
DYNAMIC_SLOTS = 1 << 10 // 1024 // 动态slot
HOISTED = -1 // 静态节点
// 指示在diff算法中退出优化模式
BALL = -2
TEXT = 1 // 动态文本节点
CLASS=1<<1,1 // 2//动态class
STYLE=1<<2,// 4 //动态style
PROPS=1<<3,// 8 //动态属性,但不包含类名和样式
FULLPR0PS=1<<4,// 16 //具有动态key属性,当key改变时,需要进行完整的diff比较。
HYDRATE_ EVENTS = 1 << 5,// 32 //带有监听事件的节点
STABLE FRAGMENT = 1 << 6, // 64 //一个不会改变子节点顺序的fragment
KEYED_ FRAGMENT = 1 << 7, // 128 //带有key属性的fragment 或部分子字节有key
UNKEYED FRAGMENT = 1<< 8, // 256 //子节点没有key 的fragment
NEED PATCH = 1 << 9, // 512 //一个节点只会进行非props比较
DYNAMIC_SLOTS = 1 << 10 // 1024 // 动态slot
HOISTED = -1 // 静态节点
// 指示在diff算法中退出优化模式
BALL = -2

2. hoistStatic 静态提升

  • Vue 2x : 无论元素是否参与更新,每次都会重新创建。
  • Vue 3x : 对不参与更新的元素,会做静态提升,只会被创建一次,之后会在每次渲染时候被不停的复用。这样就免去了重复的创建节点,大型应用会受益于这个改动,免去了重复的创建操作,优化了运行时候的内存占用
  • 将静态节点的定义,提升到父作用域,缓存起来
  • 多个相邻的静态节点,会被合并起来
  • 典型的拿空间换时间的优化策略
html
<p>HelloWorld</p>
<p>HelloWorld</p>

<p>{ message }</p>
<p>HelloWorld</p>
<p>HelloWorld</p>

<p>{ message }</p>

开启静态提升前

js
export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (
    _openBlock(),
    _createBlock('div', null, [
      _createVNode('p', null, "'HelloWorld'"),
      _createVNode('p', null, "'HelloWorld'"),
      _createVNode('p', null, _toDisplayString(_ctx.msg), 1 /* TEXT */),
    ])
  )
}
export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (
    _openBlock(),
    _createBlock('div', null, [
      _createVNode('p', null, "'HelloWorld'"),
      _createVNode('p', null, "'HelloWorld'"),
      _createVNode('p', null, _toDisplayString(_ctx.msg), 1 /* TEXT */),
    ])
  )
}

开启静态提升后编译结果

js
// https://vue-next-template-explorer.netlify.app 中打开查看编译结果

// 之后函数怎么执行,这些变量都不会被重复定义一遍
const _hoisted_1 = /*#__PURE__*/ _createVNode('p', null, "'HelloWorld'", -1 /* HOISTED */)
const _hoisted_2 = /*#__PURE__*/ _createVNode('p', null, "'HelloWorld'", -1 /* HOISTED */)

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (
    _openBlock(),
    _createBlock('div', null, [
      _hoisted_1,
      _hoisted_2,
      _createVNode('p', null, _toDisplayString(_ctx.msg), 1 /* TEXT */),
    ])
  )
}
// https://vue-next-template-explorer.netlify.app 中打开查看编译结果

// 之后函数怎么执行,这些变量都不会被重复定义一遍
const _hoisted_1 = /*#__PURE__*/ _createVNode('p', null, "'HelloWorld'", -1 /* HOISTED */)
const _hoisted_2 = /*#__PURE__*/ _createVNode('p', null, "'HelloWorld'", -1 /* HOISTED */)

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (
    _openBlock(),
    _createBlock('div', null, [
      _hoisted_1,
      _hoisted_2,
      _createVNode('p', null, _toDisplayString(_ctx.msg), 1 /* TEXT */),
    ])
  )
}

可以看到开启了静态提升后,直接将那两个内容为helloworld的p标签声明在外面了,直接就拿来用了。同时 _hoisted_1和_hoisted_2 被打上了 PatchFlag ,静态标记值为 -1 ,特殊标志是负整数表示永远不会用于 Diff

vue
<!-- https://vue-next-template-explorer.netlify.app 中打开查看编译结果:options开启hoistStatic -->
<!-- 当相同的节点达到一定阈值后会被vue3合并起来 -->
<div>
  <span>hello vue3</span>
  <span>hello vue3</span>
  <span>hello vue3</span>
  <span>hello vue3</span>
  <span>hello vue3</span>
  <span>hello vue3</span>
  <span>hello vue3</span>
  <span>hello vue3</span>
  <span>hello vue3</span>
  <span>hello vue3</span>
  <span>{{msg}}</span>
</div>

// 编译之后 import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString,
createStaticVNode as _createStaticVNode, openBlock as _openBlock, createElementBlock as
_createElementBlock } from "vue" // 多个相邻的静态节点,会被合并起来 const _hoisted_1 =
/*#__PURE__*/_createStaticVNode("
<span>hello vue3</span>
<span>hello vue3</span>
<span>hello vue3</span>
<span>hello vue3</span>
<span>hello vue3</span>
<span>hello vue3</span>
<span>hello vue3</span>
<span>hello vue3</span>
<span>hello vue3</span>
<span>hello vue3</span>
", 10) export function render(_ctx, _cache, $props, $setup, $data, $options) { return (_openBlock(),
_createElementBlock("div", null, [ _hoisted_1, _createElementVNode("span", null,
_toDisplayString(_ctx.msg), 1 /* TEXT */) ])) }
<!-- https://vue-next-template-explorer.netlify.app 中打开查看编译结果:options开启hoistStatic -->
<!-- 当相同的节点达到一定阈值后会被vue3合并起来 -->
<div>
  <span>hello vue3</span>
  <span>hello vue3</span>
  <span>hello vue3</span>
  <span>hello vue3</span>
  <span>hello vue3</span>
  <span>hello vue3</span>
  <span>hello vue3</span>
  <span>hello vue3</span>
  <span>hello vue3</span>
  <span>hello vue3</span>
  <span>{{msg}}</span>
</div>

// 编译之后 import { createElementVNode as _createElementVNode, toDisplayString as _toDisplayString,
createStaticVNode as _createStaticVNode, openBlock as _openBlock, createElementBlock as
_createElementBlock } from "vue" // 多个相邻的静态节点,会被合并起来 const _hoisted_1 =
/*#__PURE__*/_createStaticVNode("
<span>hello vue3</span>
<span>hello vue3</span>
<span>hello vue3</span>
<span>hello vue3</span>
<span>hello vue3</span>
<span>hello vue3</span>
<span>hello vue3</span>
<span>hello vue3</span>
<span>hello vue3</span>
<span>hello vue3</span>
", 10) export function render(_ctx, _cache, $props, $setup, $data, $options) { return (_openBlock(),
_createElementBlock("div", null, [ _hoisted_1, _createElementVNode("span", null,
_toDisplayString(_ctx.msg), 1 /* TEXT */) ])) }

3. cacheHandlers 事件监听缓存

  • 默认情况下绑定事件会被视为动态绑定,所以每次都会去追踪它的变化
  • 但是因为是同一个函数,所以没有追踪变化,直接缓存起来复用即可
html
<!-- https://vue-next-template-explorer.netlify.app 中打开查看编译结果:options开启cacheHandler -->
<div>
  <button @click="onClick">点我</button>
</div>
<!-- https://vue-next-template-explorer.netlify.app 中打开查看编译结果:options开启cacheHandler -->
<div>
  <button @click="onClick">点我</button>
</div>

开启事件侦听器缓存之前:

js
export const render = /*#__PURE__*/ _withId(function render(
  _ctx,
  _cache,
  $props,
  $setup,
  $data,
  $options,
) {
  return (
    _openBlock(),
    _createBlock('div', null, [
      _createVNode('button', { onClick: _ctx.onClick }, '点我', 8 /* PROPS */, ['onClick']),
      // PROPS=1<<3,// 8 //动态属性,但不包含类名和样式
    ])
  )
})
export const render = /*#__PURE__*/ _withId(function render(
  _ctx,
  _cache,
  $props,
  $setup,
  $data,
  $options,
) {
  return (
    _openBlock(),
    _createBlock('div', null, [
      _createVNode('button', { onClick: _ctx.onClick }, '点我', 8 /* PROPS */, ['onClick']),
      // PROPS=1<<3,// 8 //动态属性,但不包含类名和样式
    ])
  )
})

这里有一个8,表示着这个节点有了静态标记,有静态标记就会进行diff算法对比差异,所以会浪费时间

开启事件侦听器缓存之后:

js
export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (
    _openBlock(),
    _createBlock('div', null, [
      _createVNode(
        'button',
        {
          onClick: _cache[1] || (_cache[1] = (...args) => _ctx.onClick(...args)),
        },
        '点我',
      ),
    ])
  )
}
export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (
    _openBlock(),
    _createBlock('div', null, [
      _createVNode(
        'button',
        {
          onClick: _cache[1] || (_cache[1] = (...args) => _ctx.onClick(...args)),
        },
        '点我',
      ),
    ])
  )
}

上述发现开启了缓存后,没有了静态标记。也就是说下次diff算法的时候直接使用

4. SSR 优化

  • 静态节点直接输出,绕过了vdom
  • 动态节点,还是需要动态渲染

当静态内容大到一定量级时候,会用createStaticVNode方法在客户端去生成一个static node,这些静态node,会被直接innerHtml,就不需要创建对象,然后根据对象渲染

html
<!-- https://vue-next-template-explorer.netlify.app 中打开查看编译结果:options开启ssr -->
<div>
  <span>hello vue3</span>
  <span>hello vue3</span>
  <span>hello vue3</span>
  <!-- 这里有很多个静态节点... -->
  <span>{{msgs}}</span>
</div>



// 编译之后

import { mergeProps as _mergeProps } from "vue"
import { ssrRenderAttrs as _ssrRenderAttrs, ssrInterpolate as _ssrInterpolate } from "vue/server-renderer"

export function ssrRender(_ctx, _push, _parent, _attrs, $props, $setup, $data, $options) {
  const _cssVars = { style: { color: _ctx.color }}
  _push(`<div${
    _ssrRenderAttrs(_mergeProps(_attrs, _cssVars))
  }><span>hello vue3</span><span>hello vue3</span><span>hello vue3</span><span>${ // 静态节点直接输出
    _ssrInterpolate(_ctx.msgs)
  }</span></div>`)
}
<!-- https://vue-next-template-explorer.netlify.app 中打开查看编译结果:options开启ssr -->
<div>
  <span>hello vue3</span>
  <span>hello vue3</span>
  <span>hello vue3</span>
  <!-- 这里有很多个静态节点... -->
  <span>{{msgs}}</span>
</div>



// 编译之后

import { mergeProps as _mergeProps } from "vue"
import { ssrRenderAttrs as _ssrRenderAttrs, ssrInterpolate as _ssrInterpolate } from "vue/server-renderer"

export function ssrRender(_ctx, _push, _parent, _attrs, $props, $setup, $data, $options) {
  const _cssVars = { style: { color: _ctx.color }}
  _push(`<div${
    _ssrRenderAttrs(_mergeProps(_attrs, _cssVars))
  }><span>hello vue3</span><span>hello vue3</span><span>hello vue3</span><span>${ // 静态节点直接输出
    _ssrInterpolate(_ctx.msgs)
  }</span></div>`)
}

二、源码体积变小,使用 Tree Shaking 优化

编译时,根据不同的情况,引入不同的API,不会全部引用

  • 相比Vue2,Vue3整体体积变小了,除了移出一些不常用的API,再重要的是Tree shanking
  • 任何一个函数,如ref、reactive、computed等,仅仅在用到的时候才打包,没用到的模块都被摇掉,打包的整体体积变小
vue
<!-- https://vue-next-template-explorer.netlify.app 中打开查看编译结果 -->
<div>
  <span v-if="msg">hello vue3</span>
  <input v-model="msg" />
</div>

// 编译之后 // 模板编译会根据模板写法 指令
插值以及用了特别的功能去动态的import相应的接口,需要什么就import什么,这就是tree shaking import {
openBlock as _openBlock, createElementBlock as _createElementBlock, createCommentVNode as
_createCommentVNode, vModelText as _vModelText, createElementVNode as _createElementVNode,
withDirectives as _withDirectives } from "vue" export function render(_ctx, _cache, $props, $setup,
$data, $options) { return (_openBlock(), _createElementBlock("div", null, [ (_ctx.msg) ?
(_openBlock(), _createElementBlock("span", { key: 0 }, "hello vue3")) : _createCommentVNode("v-if",
true), _withDirectives(_createElementVNode("input", { "onUpdate:modelValue": $event => ((_ctx.msg) =
$event) }, null, 8 /* PROPS */, ["onUpdate:modelValue"]), [ [_vModelText, _ctx.msg] ]) ])) }
<!-- https://vue-next-template-explorer.netlify.app 中打开查看编译结果 -->
<div>
  <span v-if="msg">hello vue3</span>
  <input v-model="msg" />
</div>

// 编译之后 // 模板编译会根据模板写法 指令
插值以及用了特别的功能去动态的import相应的接口,需要什么就import什么,这就是tree shaking import {
openBlock as _openBlock, createElementBlock as _createElementBlock, createCommentVNode as
_createCommentVNode, vModelText as _vModelText, createElementVNode as _createElementVNode,
withDirectives as _withDirectives } from "vue" export function render(_ctx, _cache, $props, $setup,
$data, $options) { return (_openBlock(), _createElementBlock("div", null, [ (_ctx.msg) ?
(_openBlock(), _createElementBlock("span", { key: 0 }, "hello vue3")) : _createCommentVNode("v-if",
true), _withDirectives(_createElementVNode("input", { "onUpdate:modelValue": $event => ((_ctx.msg) =
$event) }, null, 8 /* PROPS */, ["onUpdate:modelValue"]), [ [_vModelText, _ctx.msg] ]) ])) }

三、响应式系统

vue2中采用 defineProperty来劫持整个对象,然后进行深度遍历所有属性,给每个属性添加getter和setter,实现响应式

vue3采用proxy重写了响应式系统,因为proxy可以对整个对象进行监听,所以不需要深度遍历

  • 可以监听动态属性的添加
  • 可以监听到数组的索引和数组length属性
  • 可以监听删除属性

Composition API 与 Options API 有什么不同 ​

分析

Vue3最重要更新之一就是Composition API,它具有一些列优点,其中不少是针对Options API暴露的一些问题量身打造。是Vue3推荐的写法,因此掌握好Composition API应用对掌握好Vue3至关重要

img

What is Composition API?(opens new window)

  • Composition API出现就是为了解决 Options API 导致相同功能代码分散的现象

img img

体验

Composition API能更好的组织代码,下面用composition api可以提取为useCount(),用于组合、复用

img

compositon api 提供了以下几个函数:

  • setup
  • ref
  • reactive
  • watchEffect
  • watch
  • computed
  • toRefs
  • 生命周期的hooks

回答范例

  1. Composition API是一组API,包括:Reactivity API、生命周期钩子、依赖注入,使用户可以通过导入函数方式编写vue组件。而Options API则通过声明组件选项的对象形式编写组件
  2. Composition API最主要作用是能够简洁、高效复用逻辑。解决了过去Options API中mixins的各种缺点;另外Composition API具有更加敏捷的代码组织能力,很多用户喜欢Options API,认为所有东西都有固定位置的选项放置代码,但是单个组件增长过大之后这反而成为限制,一个逻辑关注点分散在组件各处,形成代码碎片,维护时需要反复横跳,Composition API则可以将它们有效组织在一起。最后Composition API拥有更好的类型推断,对 ts 支持更友好,Options API在设计之初并未考虑类型推断因素,虽然官方为此做了很多复杂的类型体操,确保用户可以在使用Options API时获得类型推断,然而还是没办法用在mixins和provide/inject上
  3. Vue3首推Composition API,但是这会让我们在代码组织上多花点心思,因此在选择上,如果我们项目属于中低复杂度的场景,Options API仍是一个好选择。对于那些大型,高扩展,强维护的项目上,Composition API会获得更大收益

可能的追问

  1. Composition API能否和Options API一起使用?

可以在同一个组件中使用两个script标签,一个使用 vue3,一个使用 vue2 写法,一起使用没有问题

html
<!-- vue3 -->
<script setup>
  // vue3写法
</script>

<!-- 降级vue2 -->
<script>
  export default {
    data() {},
    methods: {},
  }
</script>
<!-- vue3 -->
<script setup>
  // vue3写法
</script>

<!-- 降级vue2 -->
<script>
  export default {
    data() {},
    methods: {},
  }
</script>

ref 如何使用 ​

ref

  • 生成值类型的响应式数据
  • 可用于模板和reactive
  • 通过.value修改值
vue
<template>
  <p>ref demo {{ ageRef }} {{ state.name }}</p>
</template>

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

export default {
  name: 'Ref',
  setup() {
    const ageRef = ref(20) // 值类型 响应式
    const nameRef = ref('test')

    const state = reactive({
      name: nameRef,
    })

    setTimeout(() => {
      console.log('ageRef', ageRef.value)

      ageRef.value = 25 // .value 修改值
      nameRef.value = 'testA'
    }, 1500)

    return {
      ageRef,
      state,
    }
  },
}
</script>

<!-- ref获取dom节点 -->
<template>
  <p ref="elemRef">我是一行文字</p>
</template>

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

export default {
  name: 'RefTemplate',
  setup() {
    const elemRef = ref(null)

    onMounted(() => {
      console.log('ref template', elemRef.value.innerHTML, elemRef.value)
    })

    return {
      elemRef,
    }
  },
}
</script>
<template>
  <p>ref demo {{ ageRef }} {{ state.name }}</p>
</template>

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

export default {
  name: 'Ref',
  setup() {
    const ageRef = ref(20) // 值类型 响应式
    const nameRef = ref('test')

    const state = reactive({
      name: nameRef,
    })

    setTimeout(() => {
      console.log('ageRef', ageRef.value)

      ageRef.value = 25 // .value 修改值
      nameRef.value = 'testA'
    }, 1500)

    return {
      ageRef,
      state,
    }
  },
}
</script>

<!-- ref获取dom节点 -->
<template>
  <p ref="elemRef">我是一行文字</p>
</template>

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

export default {
  name: 'RefTemplate',
  setup() {
    const elemRef = ref(null)

    onMounted(() => {
      console.log('ref template', elemRef.value.innerHTML, elemRef.value)
    })

    return {
      elemRef,
    }
  },
}
</script>

toRef 和 toRefs 如何使用和最佳方式 ​

toRef

  • 针对一个响应式对象(reactive封装的)的一个属性,创建一个ref,具有响应式
  • 两者保持引用关系

toRefs

  • 将响应式对象(reactive封装的)转化为普通对象
  • 对象的每个属性都是对象的ref
  • 两者保持引用关系

合成函数返回响应式对象

img

最佳使用方式

  • 用reactive做对象的响应式,用ref做值类型响应式(基本类型)
  • setup中返回toRefs(state),或者toRef(state, 'prop')
  • ref的变量命名都用xxRef
  • 合成函数返回响应式对象时,使用toRefs,有助于使用方对数据进行解构时,不丢失响应式
vue
<template>
  <p>toRef demo - {{ ageRef }} - {{ state.name }} {{ state.age }}</p>
</template>

<script>
import { ref, toRef, reactive } from 'vue'

export default {
  name: 'ToRef',
  setup() {
    const state = reactive({
      age: 20,
      name: 'test',
    })

    const age1 = computed(() => {
      return state.age + 1
    })

    // toRef 如果用于普通对象(非响应式对象),产出的结果不具备响应式
    // const state = {
    //     age: 20,
    //     name: 'test'
    // }
    // 一个响应式对象state其中一个属性要单独拿出来实现响应式用toRef
    const ageRef = toRef(state, 'age')

    setTimeout(() => {
      state.age = 25
    }, 1500)

    setTimeout(() => {
      ageRef.value = 30 // .value 修改值
    }, 3000)

    return {
      state,
      ageRef,
    }
  },
}
</script>

<template>
  <p>toRefs demo {{ age }} {{ name }}</p>
</template>

<script>
import { ref, toRef, toRefs, reactive } from 'vue'

export default {
  name: 'ToRefs',
  setup() {
    const state = reactive({
      age: 20,
      name: 'test',
    })

    const stateAsRefs = toRefs(state) // 将响应式对象,变成普通对象

    // const { age: ageRef, name: nameRef } = stateAsRefs // 每个属性,都是 ref 对象
    // return {
    //     ageRef,
    //     nameRef
    // }

    setTimeout(() => {
      state.age = 25
    }, 1500)

    return stateAsRefs
  },
}
</script>
<template>
  <p>toRef demo - {{ ageRef }} - {{ state.name }} {{ state.age }}</p>
</template>

<script>
import { ref, toRef, reactive } from 'vue'

export default {
  name: 'ToRef',
  setup() {
    const state = reactive({
      age: 20,
      name: 'test',
    })

    const age1 = computed(() => {
      return state.age + 1
    })

    // toRef 如果用于普通对象(非响应式对象),产出的结果不具备响应式
    // const state = {
    //     age: 20,
    //     name: 'test'
    // }
    // 一个响应式对象state其中一个属性要单独拿出来实现响应式用toRef
    const ageRef = toRef(state, 'age')

    setTimeout(() => {
      state.age = 25
    }, 1500)

    setTimeout(() => {
      ageRef.value = 30 // .value 修改值
    }, 3000)

    return {
      state,
      ageRef,
    }
  },
}
</script>

<template>
  <p>toRefs demo {{ age }} {{ name }}</p>
</template>

<script>
import { ref, toRef, toRefs, reactive } from 'vue'

export default {
  name: 'ToRefs',
  setup() {
    const state = reactive({
      age: 20,
      name: 'test',
    })

    const stateAsRefs = toRefs(state) // 将响应式对象,变成普通对象

    // const { age: ageRef, name: nameRef } = stateAsRefs // 每个属性,都是 ref 对象
    // return {
    //     ageRef,
    //     nameRef
    // }

    setTimeout(() => {
      state.age = 25
    }, 1500)

    return stateAsRefs
  },
}
</script>

深入理解为什么需要 ref、toRef、toRefs ​

为什么需要用 ref

  • 返回值类型,会丢失响应式
  • 如在setup、computed、合成函数,都有可能返回值类型
  • Vue如不定义ref,用户将制造ref,反而更混乱

为何 ref 需要.value 属性

  • ref是一个对象(不丢失响应式),value存储值
  • 通过.value属性的get和set实现响应式
  • 用于模板、reactive时,不需要.value,其他情况都要

为什么需要 toRef 和 toRefs

  • 初衷:不丢失响应式的情况下,把对象数据 分解/扩散
  • 前端:针对的是响应式对象(reactive封装的)非普通对象
  • 注意:不创造响应式,而是延续响应式
vue
<template>
  <p>why ref demo {{ state.age }} - {{ age1 }}</p>
</template>

<script>
import { ref, toRef, toRefs, reactive, computed } from 'vue'

function useFeatureX() {
  const state = reactive({
    x: 1,
    y: 2,
  })

  return toRefs(state)
}

export default {
  name: 'WhyRef',
  setup() {
    // 解构不丢失响应式
    const { x, y } = useFeatureX()

    const state = reactive({
      age: 20,
      name: 'test',
    })

    // computed 返回的是一个类似于 ref 的对象,也有 .value
    const age1 = computed(() => {
      return state.age + 1
    })

    setTimeout(() => {
      state.age = 25
    }, 1500)

    return {
      state,
      age1,
      x,
      y,
    }
  },
}
</script>
<template>
  <p>why ref demo {{ state.age }} - {{ age1 }}</p>
</template>

<script>
import { ref, toRef, toRefs, reactive, computed } from 'vue'

function useFeatureX() {
  const state = reactive({
    x: 1,
    y: 2,
  })

  return toRefs(state)
}

export default {
  name: 'WhyRef',
  setup() {
    // 解构不丢失响应式
    const { x, y } = useFeatureX()

    const state = reactive({
      age: 20,
      name: 'test',
    })

    // computed 返回的是一个类似于 ref 的对象,也有 .value
    const age1 = computed(() => {
      return state.age + 1
    })

    setTimeout(() => {
      state.age = 25
    }, 1500)

    return {
      state,
      age1,
      x,
      y,
    }
  },
}
</script>

ref 和 reactive 异同 ​

这是Vue3数据响应式中非常重要的两个概念,跟我们写代码关系也很大

js
const count = ref(0)
console.log(count.value) // 0

count.value++
console.log(count.value) // 1

const obj = reactive({ count: 0 })
obj.count++
const count = ref(0)
console.log(count.value) // 0

count.value++
console.log(count.value) // 1

const obj = reactive({ count: 0 })
obj.count++
  • ref接收内部值(inner value)返回响应式Ref对象,reactive返回响应式代理对象
  • 从定义上看ref通常用于处理单值的响应式,reactive用于处理对象类型的数据响应式
  • 两者均是用于构造响应式数据,但是ref主要解决原始值的响应式问题
  • ref返回的响应式数据在 JS 中使用需要加上.value才能访问其值,在视图中使用会自动脱ref,不需要.value;ref可以接收对象或数组等非原始值,但内部依然是reactive实现响应式;reactive内部如果接收Ref 对象会自动脱ref;使用展开运算符(...)展开reactive返回的响应式对象会使其失去响应性,可以结合toRefs()将值转换为Ref对象之后再展开。
  • reactive内部使用Proxy代理传入对象并拦截该对象各种操作,从而实现响应式。ref内部封装一个RefImpl类,并设置get value/set value,拦截用户对值的访问,从而实现响应式

vue3 升级了哪些重要功能 ​

1. createApp

js
// vue2
const app = new Vue({
  /**选项**/
})
Vue.use(/****/)
Vue.mixin(/****/)
Vue.component(/****/)
Vue.directive(/****/)

// vue3
const app = createApp({
  /**选项**/
})
app.use(/****/)
app.mixin(/****/)
app.component(/****/)
app.directive(/****/)
// vue2
const app = new Vue({
  /**选项**/
})
Vue.use(/****/)
Vue.mixin(/****/)
Vue.component(/****/)
Vue.directive(/****/)

// vue3
const app = createApp({
  /**选项**/
})
app.use(/****/)
app.mixin(/****/)
app.component(/****/)
app.directive(/****/)

2. emits 属性

js
// 父组件
<Hello :msg="msg" @onSayHello="sayHello">

// 子组件
export default {
    name: 'Hello',
    props: {
        msg: String
    },
    emits: ['onSayHello'], // 声明emits
    setup(props, {emit}) {
        emit('onSayHello', 'aaa')
    }
}
// 父组件
<Hello :msg="msg" @onSayHello="sayHello">

// 子组件
export default {
    name: 'Hello',
    props: {
        msg: String
    },
    emits: ['onSayHello'], // 声明emits
    setup(props, {emit}) {
        emit('onSayHello', 'aaa')
    }
}

3. 多事件

vue
<!-- 定义多个事件 -->
<button @click="one($event), two($event)">提交</button>
<!-- 定义多个事件 -->
<button @click="one($event), two($event)">提交</button>

4. Fragment

vue
<!-- vue2 -->
<template>
  <div>
    <h2>{{ title }}</h2>
    <p>test</p>
  </div>
</template>

<!-- vue3:不在使用div节点包裹 -->
<template>
  <h2>{{ title }}</h2>
  <p>test</p>
</template>
<!-- vue2 -->
<template>
  <div>
    <h2>{{ title }}</h2>
    <p>test</p>
  </div>
</template>

<!-- vue3:不在使用div节点包裹 -->
<template>
  <h2>{{ title }}</h2>
  <p>test</p>
</template>

5. 移除.sync

vue
<!-- vue2 -->
<MyComponent :title.sync="title" />

<!-- vue3 简写 -->
<MyComponent v-model:title="title" />
<!-- 非简写 -->
<MyComponent :title="title" @update:title="title = $event" />
<!-- vue2 -->
<MyComponent :title.sync="title" />

<!-- vue3 简写 -->
<MyComponent v-model:title="title" />
<!-- 非简写 -->
<MyComponent :title="title" @update:title="title = $event" />

.sync 用法

父组件把属性给子组件,子组件修改了后还能同步到父组件中来

vue
<template>
  <button @click="close">关闭</button>
</template>
<script>
export default {
  props: {
    isVisible: {
      type: Boolean,
      default: false,
    },
  },
  methods: {
    close() {
      this.$emit('update:isVisible', false)
    },
  },
}
</script>

<!-- 父组件使用 -->
<chlid-component :isVisible.sync="isVisible"></chlid-component>

<text-doc :title="doc.title" @update:title="doc.title = $event"></text-doc>

<!-- 为了方便期间,为这种模式提供一个简写 .sync -->
<text-doc :title.sync="doc.title" />
<template>
  <button @click="close">关闭</button>
</template>
<script>
export default {
  props: {
    isVisible: {
      type: Boolean,
      default: false,
    },
  },
  methods: {
    close() {
      this.$emit('update:isVisible', false)
    },
  },
}
</script>

<!-- 父组件使用 -->
<chlid-component :isVisible.sync="isVisible"></chlid-component>

<text-doc :title="doc.title" @update:title="doc.title = $event"></text-doc>

<!-- 为了方便期间,为这种模式提供一个简写 .sync -->
<text-doc :title.sync="doc.title" />

6. 异步组件的写法

js
// vue2写法
new Vue({
  components: {
    'my-component': () => import('./my-component.vue'),
  },
})

// vue3写法
import { createApp, defineAsyncComponent } from 'vue'

export default {
  components: {
    AsyncComponent: defineAsyncComponent(() => import('./AsyncComponent.vue')),
  },
}
// vue2写法
new Vue({
  components: {
    'my-component': () => import('./my-component.vue'),
  },
})

// vue3写法
import { createApp, defineAsyncComponent } from 'vue'

export default {
  components: {
    AsyncComponent: defineAsyncComponent(() => import('./AsyncComponent.vue')),
  },
}

7. 移除 filter

vue
<!-- 以下filter在vue3中不可用了 -->

<!-- 在花括号中 -->
{{ message | capitalize }}

<!-- 在v-bind中 -->
<div v-bind:id="rawId | formatId"></div>
<!-- 以下filter在vue3中不可用了 -->

<!-- 在花括号中 -->
{{ message | capitalize }}

<!-- 在v-bind中 -->
<div v-bind:id="rawId | formatId"></div>

8. Teleport

vue
<button @click="modalOpen = true">
 open
</button>

<!-- 通过teleport把弹窗放到body下 -->
<teleport to="body">
 <div v-if="modalOpen" classs="modal">
   <div>
     teleport弹窗,父元素是body
     <button @click="modalOpen = false">close</button>
   </div>
 </div>
</teleport>
<button @click="modalOpen = true">
 open
</button>

<!-- 通过teleport把弹窗放到body下 -->
<teleport to="body">
 <div v-if="modalOpen" classs="modal">
   <div>
     teleport弹窗,父元素是body
     <button @click="modalOpen = false">close</button>
   </div>
 </div>
</teleport>

9. Suspense

html
<Suspense>
  <template>
    <!-- 异步组件 -->
    <Test1 />
  </template>
  <!-- fallback是一个具名插槽,即Suspense内部有两个slot,一个具名插槽fallback -->
  <template #fallback>loading...</template>
</Suspense>
<Suspense>
  <template>
    <!-- 异步组件 -->
    <Test1 />
  </template>
  <!-- fallback是一个具名插槽,即Suspense内部有两个slot,一个具名插槽fallback -->
  <template #fallback>loading...</template>
</Suspense>

10. Composition API

  • reactive
  • ref
  • readonly
  • watch和watchEffect
  • setup
  • 生命周期钩子函数

Vue3.2 setup 语法糖汇总 ​

提示:vue3.2 版本开始才能使用语法糖!

在 Vue3.0 中变量必须 return 出来, template 中才能使用;而在 Vue3.2 中只需要在 script 标签上加上 setup 属性,无需 return, template 便可直接使用,非常的香啊!

1. 如何使用 setup 语法糖

只需在 script 标签上写上 setup

html
<template></template>
<script setup></script>
<style scoped lang="less"></style>
<template></template>
<script setup></script>
<style scoped lang="less"></style>

2. data 数据的使用

由于 setup 不需写 return ,所以直接声明数据即可

html
<script setup>
  import { ref, reactive, toRefs } from 'vue'

  const data = reactive({
    patternVisible: false,
    debugVisible: false,
    aboutExeVisible: false,
  })

  const content = ref('content')
  //使用toRefs解构
  const { patternVisible, debugVisible, aboutExeVisible } = toRefs(data)
</script>
<script setup>
  import { ref, reactive, toRefs } from 'vue'

  const data = reactive({
    patternVisible: false,
    debugVisible: false,
    aboutExeVisible: false,
  })

  const content = ref('content')
  //使用toRefs解构
  const { patternVisible, debugVisible, aboutExeVisible } = toRefs(data)
</script>

3. method 方法的使用

html
<template>
  <button @click="onClickHelp">帮助</button>
</template>
<script setup>
  import { reactive } from 'vue'

  const data = reactive({
    aboutExeVisible: false,
  })
  // 点击帮助
  const onClickHelp = () => {
    console.log(`帮助`)
    data.aboutExeVisible = true
  }
</script>
<template>
  <button @click="onClickHelp">帮助</button>
</template>
<script setup>
  import { reactive } from 'vue'

  const data = reactive({
    aboutExeVisible: false,
  })
  // 点击帮助
  const onClickHelp = () => {
    console.log(`帮助`)
    data.aboutExeVisible = true
  }
</script>

4. watchEffect 的使用

html
<script setup>
  import { ref, watchEffect } from 'vue'

  let sum = ref(0)

  watchEffect(() => {
    const x1 = sum.value
    console.log('watchEffect所指定的回调执行了')
  })
</script>
<script setup>
  import { ref, watchEffect } from 'vue'

  let sum = ref(0)

  watchEffect(() => {
    const x1 = sum.value
    console.log('watchEffect所指定的回调执行了')
  })
</script>

5. watch 的使用

html
<script setup>
  import { reactive, watch } from 'vue'
  //数据
  let sum = ref(0)
  let msg = ref('hello')
  let person = reactive({
    name: '张三',
    age: 18,
    job: {
      j1: {
        salary: 20,
      },
    },
  })
  // 两种监听格式
  watch(
    [sum, msg],
    (newValue, oldValue) => {
      console.log('sum或msg变了', newValue, oldValue)
    },
    { immediate: true },
  )

  watch(
    () => person.job,
    (newValue, oldValue) => {
      console.log('person的job变化了', newValue, oldValue)
    },
    { deep: true },
  )
</script>
<script setup>
  import { reactive, watch } from 'vue'
  //数据
  let sum = ref(0)
  let msg = ref('hello')
  let person = reactive({
    name: '张三',
    age: 18,
    job: {
      j1: {
        salary: 20,
      },
    },
  })
  // 两种监听格式
  watch(
    [sum, msg],
    (newValue, oldValue) => {
      console.log('sum或msg变了', newValue, oldValue)
    },
    { immediate: true },
  )

  watch(
    () => person.job,
    (newValue, oldValue) => {
      console.log('person的job变化了', newValue, oldValue)
    },
    { deep: true },
  )
</script>

6. computed 计算属性的使用

computed 计算属性有两种写法(简写和考虑读写的完整写法)

html
<script setup>
  import { reactive, computed } from 'vue'

  // 数据
  let person = reactive({
    firstName: 'poetry',
    lastName: 'x',
  })

  // 计算属性简写
  person.fullName = computed(() => {
    return person.firstName + '-' + person.lastName
  })

  // 完整写法
  person.fullName = computed({
    get() {
      return person.firstName + '-' + person.lastName
    },
    set(value) {
      const nameArr = value.split('-')
      person.firstName = nameArr[0]
      person.lastName = nameArr[1]
    },
  })
</script>
<script setup>
  import { reactive, computed } from 'vue'

  // 数据
  let person = reactive({
    firstName: 'poetry',
    lastName: 'x',
  })

  // 计算属性简写
  person.fullName = computed(() => {
    return person.firstName + '-' + person.lastName
  })

  // 完整写法
  person.fullName = computed({
    get() {
      return person.firstName + '-' + person.lastName
    },
    set(value) {
      const nameArr = value.split('-')
      person.firstName = nameArr[0]
      person.lastName = nameArr[1]
    },
  })
</script>

7. props 父子传值的使用

父组件代码如下(示例):

html
<template>
  <child :name="name" />
</template>

<script setup>
  import { ref } from 'vue'
  // 引入子组件
  import child from './child.vue'
  let name = ref('poetry')
</script>
<template>
  <child :name="name" />
</template>

<script setup>
  import { ref } from 'vue'
  // 引入子组件
  import child from './child.vue'
  let name = ref('poetry')
</script>

子组件代码如下(示例):

html
<template>
  <span>{{props.name}}</span>
</template>

<script setup>
  import { defineProps } from 'vue'
  // 声明props
  const props = defineProps({
    name: {
      type: String,
      default: 'poetries',
    },
  })
  // 或者
  //const props = defineProps(['name'])
</script>
<template>
  <span>{{props.name}}</span>
</template>

<script setup>
  import { defineProps } from 'vue'
  // 声明props
  const props = defineProps({
    name: {
      type: String,
      default: 'poetries',
    },
  })
  // 或者
  //const props = defineProps(['name'])
</script>

8. emit 子父传值的使用

父组件代码如下(示例):

html
<template>
  <AdoutExe @aboutExeVisible="aboutExeHandleCancel" />
</template>
<script setup>
  import { reactive } from 'vue'
  // 导入子组件
  import AdoutExe from '../components/AdoutExeCom'

  const data = reactive({
    aboutExeVisible: false,
  })
  // content组件ref

  // 关于系统隐藏
  const aboutExeHandleCancel = () => {
    data.aboutExeVisible = false
  }
</script>
<template>
  <AdoutExe @aboutExeVisible="aboutExeHandleCancel" />
</template>
<script setup>
  import { reactive } from 'vue'
  // 导入子组件
  import AdoutExe from '../components/AdoutExeCom'

  const data = reactive({
    aboutExeVisible: false,
  })
  // content组件ref

  // 关于系统隐藏
  const aboutExeHandleCancel = () => {
    data.aboutExeVisible = false
  }
</script>

子组件代码如下(示例):

html
<template>
  <a-button @click="isOk">确定</a-button>
</template>
<script setup>
  import { defineEmits } from 'vue'

  // emit
  const emit = defineEmits(['aboutExeVisible'])
  /**
   * 方法
   */
  // 点击确定按钮
  const isOk = () => {
    emit('aboutExeVisible')
  }
</script>
<template>
  <a-button @click="isOk">确定</a-button>
</template>
<script setup>
  import { defineEmits } from 'vue'

  // emit
  const emit = defineEmits(['aboutExeVisible'])
  /**
   * 方法
   */
  // 点击确定按钮
  const isOk = () => {
    emit('aboutExeVisible')
  }
</script>

9. 获取子组件 ref 变量和 defineExpose 暴露

即vue2中的获取子组件的ref,直接在父组件中控制子组件方法和变量的方法

父组件代码如下(示例):

html
<template>
  <button @click="onClickSetUp">点击</button>
  <content ref="content" />
</template>

<script setup>
  import { ref } from 'vue'

  // content组件ref
  const content = ref('content')
  // 点击设置
  const onClickSetUp = ({ key }) => {
    content.value.modelVisible = true
  }
</script>
<style scoped lang="less"></style>
<template>
  <button @click="onClickSetUp">点击</button>
  <content ref="content" />
</template>

<script setup>
  import { ref } from 'vue'

  // content组件ref
  const content = ref('content')
  // 点击设置
  const onClickSetUp = ({ key }) => {
    content.value.modelVisible = true
  }
</script>
<style scoped lang="less"></style>

子组件代码如下(示例):

html
<template>
  <p>{{data }}</p>
</template>

<script setup>
  import { reactive, toRefs } from 'vue'

  /**
   * 数据部分
   * */
  const data = reactive({
    modelVisible: false,
    historyVisible: false,
    reportVisible: false,
  })

  defineExpose({
    ...toRefs(data),
  })
</script>
<template>
  <p>{{data }}</p>
</template>

<script setup>
  import { reactive, toRefs } from 'vue'

  /**
   * 数据部分
   * */
  const data = reactive({
    modelVisible: false,
    historyVisible: false,
    reportVisible: false,
  })

  defineExpose({
    ...toRefs(data),
  })
</script>

10. 路由 useRoute 和 useRouter 的使用

html
<script setup>
  import { useRoute, useRouter } from 'vue-router'

  // 声明
  const route = useRoute()
  const router = useRouter()

  // 获取query
  console.log(route.query)
  // 获取params
  console.log(route.params)

  // 路由跳转
  router.push({
    path: `/index`,
  })
</script>
<script setup>
  import { useRoute, useRouter } from 'vue-router'

  // 声明
  const route = useRoute()
  const router = useRouter()

  // 获取query
  console.log(route.query)
  // 获取params
  console.log(route.params)

  // 路由跳转
  router.push({
    path: `/index`,
  })
</script>

11. store 仓库的使用

html
<script setup>
  import { useStore } from 'vuex'
  import { num } from '../store/index'

  const store = useStore(num)

  // 获取Vuex的state
  console.log(store.state.number)
  // 获取Vuex的getters
  console.log(store.state.getNumber)

  // 提交mutations
  store.commit('fnName')

  // 分发actions的方法
  store.dispatch('fnName')
</script>
<script setup>
  import { useStore } from 'vuex'
  import { num } from '../store/index'

  const store = useStore(num)

  // 获取Vuex的state
  console.log(store.state.number)
  // 获取Vuex的getters
  console.log(store.state.getNumber)

  // 提交mutations
  store.commit('fnName')

  // 分发actions的方法
  store.dispatch('fnName')
</script>

12. await 的支持

setup`语法糖中可直接使用`await`,不需要写`async`,`setup`会自动变成`async setup
<script setup>
  import api from '../api/Api'
  const data = await Api.getData()
  console.log(data)
</script>
setup`语法糖中可直接使用`await`,不需要写`async`,`setup`会自动变成`async setup
<script setup>
  import api from '../api/Api'
  const data = await Api.getData()
  console.log(data)
</script>

13. provide 和 inject 祖孙传值

父组件代码如下(示例):

html
<template>
  <AdoutExe />
</template>

<script setup>
  import { ref, provide } from 'vue'
  import AdoutExe from '@/components/AdoutExeCom'

  let name = ref('py')
  // 使用provide
  provide('provideState', {
    name,
    changeName: () => {
      name.value = 'poetries'
    },
  })
</script>
<template>
  <AdoutExe />
</template>

<script setup>
  import { ref, provide } from 'vue'
  import AdoutExe from '@/components/AdoutExeCom'

  let name = ref('py')
  // 使用provide
  provide('provideState', {
    name,
    changeName: () => {
      name.value = 'poetries'
    },
  })
</script>

子组件代码如下(示例):

html
<script setup>
  import { inject } from 'vue'
  const provideState = inject('provideState')

  provideState.changeName()
</script>
<script setup>
  import { inject } from 'vue'
  const provideState = inject('provideState')

  provideState.changeName()
</script>

v-model 参数的用法 ​

vue2 自定义组件 model

vue
<!-- 自定义 v-model -->
<CustomVModel v-model="name" />

<!-- CustomVModel -->
<template>
  <!-- 例如:vue 颜色选择 -->
  <input type="text" :value="text" @input="$emit('change', $event.target.value)" />
  <!--
    1. 上面的 input 使用了 :value 而不是 v-model
    2. 上面的 change 和 model.event 要对应起来
    3. text 属性对应起来
  -->
</template>

<script>
export default {
  model: {
    prop: 'text', // 对应 props text
    event: 'change',
  },
  props: {
    text1: String,
    default() {
      return ''
    },
  },
}
</script>
<!-- 自定义 v-model -->
<CustomVModel v-model="name" />

<!-- CustomVModel -->
<template>
  <!-- 例如:vue 颜色选择 -->
  <input type="text" :value="text" @input="$emit('change', $event.target.value)" />
  <!--
    1. 上面的 input 使用了 :value 而不是 v-model
    2. 上面的 change 和 model.event 要对应起来
    3. text 属性对应起来
  -->
</template>

<script>
export default {
  model: {
    prop: 'text', // 对应 props text
    event: 'change',
  },
  props: {
    text1: String,
    default() {
      return ''
    },
  },
}
</script>

vue3 自定义组件 model

vue
<!-- index.vue 绑定一个值 -->
<CustomVModel v-model="name" />

<!-- CustomVModel.vue -->
<template>
  <input type="text" :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" />
</template>

<script>
export default {
  props: {
    modelValue: String,
  },
}
</script>

<!-- UserInfo组件 -->
<template>
  <input :value="name" @input="$emit('update:name', $event.target.value)" />
  <input :value="age" @input="$emit('update:age', $event.target.value)" />
</template>

<script>
export default {
  name: 'UserInfo',
  props: {
    name: String,
    age: String,
  },
}
</script>

<!-- 绑定多个值
使用 vue3中v-model可绑定多个属性 -->
<user-info v-model:name="name" v-model:age="age"></user-info>
<!-- index.vue 绑定一个值 -->
<CustomVModel v-model="name" />

<!-- CustomVModel.vue -->
<template>
  <input type="text" :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" />
</template>

<script>
export default {
  props: {
    modelValue: String,
  },
}
</script>

<!-- UserInfo组件 -->
<template>
  <input :value="name" @input="$emit('update:name', $event.target.value)" />
  <input :value="age" @input="$emit('update:age', $event.target.value)" />
</template>

<script>
export default {
  name: 'UserInfo',
  props: {
    name: String,
    age: String,
  },
}
</script>

<!-- 绑定多个值
使用 vue3中v-model可绑定多个属性 -->
<user-info v-model:name="name" v-model:age="age"></user-info>

watch 和 watchEffect 的区别 ​

  • 两者都可以监听data属性变化
  • watch需要明确监听哪个属性
  • watchEffect会根据其中的属性,自动监听其变化
vue
<template>
  <p>watch vs watchEffect</p>
  <p>{{ numberRef }}</p>
  <p>{{ name }} {{ age }}</p>
</template>

<script>
import { reactive, ref, toRefs, watch, watchEffect } from 'vue'

export default {
  name: 'Watch',
  setup() {
    const numberRef = ref(100)
    const state = reactive({
      name: 'test',
      age: 20,
    })

    watchEffect(() => {
      // 初始化时,一定会执行一次(收集要监听的数据)
      console.log('hello watchEffect')
    })
    watchEffect(() => {
      console.log('state.name', state.name)
    })
    watchEffect(() => {
      console.log('state.age', state.age)
    })
    watchEffect(() => {
      console.log('state.age', state.age)
      console.log('state.name', state.name)
    })
    setTimeout(() => {
      state.age = 25
    }, 1500)
    setTimeout(() => {
      state.name = 'testA'
    }, 3000)

    // ref直接写
    // watch(numberRef, (newNumber, oldNumber) => {
    //     console.log('ref watch', newNumber, oldNumber)
    // }
    // // , {
    // //     immediate: true // 初始化之前就监听,可选
    // // }
    // )

    // setTimeout(() => {
    //     numberRef.value = 200
    // }, 1500)

    // watch(
    //     // 第一个参数,确定要监听哪个属性
    //     () => state.age,

    //     // 第二个参数,回调函数
    //     (newAge, oldAge) => {
    //         console.log('state watch', newAge, oldAge)
    //     },

    //     // 第三个参数,配置项
    //     {
    //         immediate: true, // 初始化之前就监听,可选
    //         // deep: true // 深度监听
    //     }
    // )

    // setTimeout(() => {
    //     state.age = 25
    // }, 1500)
    // setTimeout(() => {
    //     state.name = 'zhangsanA'
    // }, 3000)

    return {
      numberRef,
      ...toRefs(state),
    }
  },
}
</script>
<template>
  <p>watch vs watchEffect</p>
  <p>{{ numberRef }}</p>
  <p>{{ name }} {{ age }}</p>
</template>

<script>
import { reactive, ref, toRefs, watch, watchEffect } from 'vue'

export default {
  name: 'Watch',
  setup() {
    const numberRef = ref(100)
    const state = reactive({
      name: 'test',
      age: 20,
    })

    watchEffect(() => {
      // 初始化时,一定会执行一次(收集要监听的数据)
      console.log('hello watchEffect')
    })
    watchEffect(() => {
      console.log('state.name', state.name)
    })
    watchEffect(() => {
      console.log('state.age', state.age)
    })
    watchEffect(() => {
      console.log('state.age', state.age)
      console.log('state.name', state.name)
    })
    setTimeout(() => {
      state.age = 25
    }, 1500)
    setTimeout(() => {
      state.name = 'testA'
    }, 3000)

    // ref直接写
    // watch(numberRef, (newNumber, oldNumber) => {
    //     console.log('ref watch', newNumber, oldNumber)
    // }
    // // , {
    // //     immediate: true // 初始化之前就监听,可选
    // // }
    // )

    // setTimeout(() => {
    //     numberRef.value = 200
    // }, 1500)

    // watch(
    //     // 第一个参数,确定要监听哪个属性
    //     () => state.age,

    //     // 第二个参数,回调函数
    //     (newAge, oldAge) => {
    //         console.log('state watch', newAge, oldAge)
    //     },

    //     // 第三个参数,配置项
    //     {
    //         immediate: true, // 初始化之前就监听,可选
    //         // deep: true // 深度监听
    //     }
    // )

    // setTimeout(() => {
    //     state.age = 25
    // }, 1500)
    // setTimeout(() => {
    //     state.name = 'zhangsanA'
    // }, 3000)

    return {
      numberRef,
      ...toRefs(state),
    }
  },
}
</script>

setup 中如何获取组件实例 ​

  • 在setup和其他composition API中没有this
  • 通过getCurrentInstance获取当前实例
  • 若使用options API可以照常使用this
js
import { onMounted, getCurrentInstance } from 'vue'

export default {
  name: 'GetInstance',
  data() {
    return {
      x: 1,
      y: 2,
    }
  },
  setup() {
    // setup是created beforeCreate 合集 组件还没正式初始化
    console.log('this1', this) // undefined

    onMounted(() => {
      console.log('this in onMounted', this) // undefined
      console.log('x', instance.data.x) // 1  onMounted中组件已经初始化了
    })

    const instance = getCurrentInstance()
    console.log('instance', instance)
  },
  mounted() {
    console.log('this2', this)
    console.log('y', this.y)
  },
}
import { onMounted, getCurrentInstance } from 'vue'

export default {
  name: 'GetInstance',
  data() {
    return {
      x: 1,
      y: 2,
    }
  },
  setup() {
    // setup是created beforeCreate 合集 组件还没正式初始化
    console.log('this1', this) // undefined

    onMounted(() => {
      console.log('this in onMounted', this) // undefined
      console.log('x', instance.data.x) // 1  onMounted中组件已经初始化了
    })

    const instance = getCurrentInstance()
    console.log('instance', instance)
  },
  mounted() {
    console.log('this2', this)
    console.log('y', this.y)
  },
}

Vite 为什么启动非常快 ​

  • 开发环境使用Es6 Module,无需打包,非常快
  • 生产环境使用rollup,并不会快很多

ES Module 在浏览器中的应用

html
<p>基本演示</p>
<script type="module">
  import add from './src/add.js'

  const res = add(1, 2)
  console.log('add res', res)
</script>
<script type="module">
  import { add, multi } from './src/math.js'
  console.log('add res', add(10, 20))
  console.log('multi res', multi(10, 20))
</script>

<p>外链引用</p>
<script type="module" src="./src/index.js"></script>

<p>远程引用</p>
<script type="module">
  import { createStore } from 'https://unpkg.com/redux@latest/es/redux.mjs' // es module规范mjs
  console.log('createStore', createStore)
</script>

<p>动态引入</p>
<button id="btn1">load1</button>
<button id="btn2">load2</button>

<script type="module">
  document.getElementById('btn1').addEventListener('click', async () => {
    const add = await import('./src/add.js')
    const res = add.default(1, 2)
    console.log('add res', res)
  })
  document.getElementById('btn2').addEventListener('click', async () => {
    const { add, multi } = await import('./src/math.js')
    console.log('add res', add(10, 20))
    console.log('multi res', multi(10, 20))
  })
</script>
<p>基本演示</p>
<script type="module">
  import add from './src/add.js'

  const res = add(1, 2)
  console.log('add res', res)
</script>
<script type="module">
  import { add, multi } from './src/math.js'
  console.log('add res', add(10, 20))
  console.log('multi res', multi(10, 20))
</script>

<p>外链引用</p>
<script type="module" src="./src/index.js"></script>

<p>远程引用</p>
<script type="module">
  import { createStore } from 'https://unpkg.com/redux@latest/es/redux.mjs' // es module规范mjs
  console.log('createStore', createStore)
</script>

<p>动态引入</p>
<button id="btn1">load1</button>
<button id="btn2">load2</button>

<script type="module">
  document.getElementById('btn1').addEventListener('click', async () => {
    const add = await import('./src/add.js')
    const res = add.default(1, 2)
    console.log('add res', res)
  })
  document.getElementById('btn2').addEventListener('click', async () => {
    const { add, multi } = await import('./src/math.js')
    console.log('add res', add(10, 20))
    console.log('multi res', multi(10, 20))
  })
</script>

说说 Vue 3.0 中 Tree shaking 特性?举例说明一下? ​

一、是什么

Tree shaking` 是一种通过清除多余代码方式来优化项目打包体积的技术,专业术语叫 `Dead code elimination
Tree shaking` 是一种通过清除多余代码方式来优化项目打包体积的技术,专业术语叫 `Dead code elimination

简单来讲,就是在保持代码运行结果不变的前提下,去除无用的代码

如果把代码打包比作制作蛋糕,传统的方式是把鸡蛋(带壳)全部丢进去搅拌,然后放入烤箱,最后把(没有用的)蛋壳全部挑选并剔除出去

而treeshaking则是一开始就把有用的蛋白蛋黄(import)放入搅拌,最后直接作出蛋糕

也就是说 ,tree shaking 其实是找出使用的代码

在Vue2中,无论我们使用什么功能,它们最终都会出现在生产代码中。主要原因是Vue实例在项目中是单例的,捆绑程序无法检测到该对象的哪些属性在代码中被使用到

js
import Vue from 'vue'

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

Vue.nextTick(() => {})

而Vue3源码引入tree shaking特性,将全局 API 进行分块。如果您不使用其某些功能,它们将不会包含在您的基础包中

js
import { nextTick, observable } from 'vue'

nextTick(() => {})
import { nextTick, observable } from 'vue'

nextTick(() => {})

二、如何做

Tree shaking是基于ES6模板语法(import与exports),主要是借助ES6模块的静态编译思想,在编译时就能确定模块的依赖关系,以及输入和输出的变量

Tree shaking无非就是做了两件事:

  • 编译阶段利用ES6 Module判断哪些模块已经加载
  • 判断那些模块和变量未被使用或者引用,进而删除对应代码

下面就来举个例子:

通过脚手架vue-cli安装Vue2与Vue3项目

c
vue create vue-demo
vue create vue-demo

Vue2 项目

组件中使用data属性

vue
<script>
export default {
  data: () => ({
    count: 1,
  }),
}
</script>
<script>
export default {
  data: () => ({
    count: 1,
  }),
}
</script>

对项目进行打包,体积如下图

img

为组件设置其他属性(compted、watch)

js
export default {
    data: () => ({
        question:"",
        count: 1,
    }),
    computed: {
        double: function () {
            return this.count * 2;
        },
    },
    watch: {
        question: function (newQuestion, oldQuestion) {
            this.answer = 'xxxx'
        }
};
export default {
    data: () => ({
        question:"",
        count: 1,
    }),
    computed: {
        double: function () {
            return this.count * 2;
        },
    },
    watch: {
        question: function (newQuestion, oldQuestion) {
            this.answer = 'xxxx'
        }
};

再一次打包,发现打包出来的体积并没有变化

img

Vue3 项目

组件中简单使用

js
import { reactive, defineComponent } from 'vue'
export default defineComponent({
  setup() {
    const state = reactive({
      count: 1,
    })
    return {
      state,
    }
  },
})
import { reactive, defineComponent } from 'vue'
export default defineComponent({
  setup() {
    const state = reactive({
      count: 1,
    })
    return {
      state,
    }
  },
})

将项目进行打包

img

在组件中引入computed和watch

js
import { reactive, defineComponent, computed, watch } from 'vue'
export default defineComponent({
  setup() {
    const state = reactive({
      count: 1,
    })
    const double = computed(() => {
      return state.count * 2
    })

    watch(
      () => state.count,
      (count, preCount) => {
        console.log(count)
        console.log(preCount)
      },
    )
    return {
      state,
      double,
    }
  },
})
import { reactive, defineComponent, computed, watch } from 'vue'
export default defineComponent({
  setup() {
    const state = reactive({
      count: 1,
    })
    const double = computed(() => {
      return state.count * 2
    })

    watch(
      () => state.count,
      (count, preCount) => {
        console.log(count)
        console.log(preCount)
      },
    )
    return {
      state,
      double,
    }
  },
})

再次对项目进行打包,可以看到在引入computer和watch之后,项目整体体积变大了

img

三、作用

通过Tree shaking,Vue3给我们带来的好处是:

  • 减少程序体积(更小)
  • 减少程序执行时间(更快)
  • 便于将来对程序架构进行优化(更友好)

用 Vue3.0 写过组件吗?如果想实现一个 Modal 你会怎么设计? ​

一、组件设计

组件就是把图形、非图形的各种逻辑均抽象为一个统一的概念(组件)来实现开发的模式

现在有一个场景,点击新增与编辑都弹框出来进行填写,功能上大同小异,可能只是标题内容或者是显示的主体内容稍微不同

这时候就没必要写两个组件,只需要根据传入的参数不同,组件显示不同内容即可

这样,下次开发相同界面程序时就可以写更少的代码,意义着更高的开发效率,更少的 Bug和更少的程序体积

二、需求分析

实现一个Modal组件,首先确定需要完成的内容:

  • 遮罩层
  • 标题内容
  • 主体内容
  • 确定和取消按钮

主体内容需要灵活,所以可以是字符串,也可以是一段 html 代码

特点是它们在当前vue实例之外独立存在,通常挂载于body之上

除了通过引入import的形式,我们还可通过API的形式进行组件的调用

还可以包括配置全局样式、国际化、与typeScript结合

三、实现流程

首先看看大致流程:

  • 目录结构
  • 组件内容
  • 实现 API 形式
  • 事件处理
  • 其他完善

目录结构

Modal组件相关的目录结构

text
├── plugins
│   └── modal
│       ├── Content.tsx // 维护 Modal 的内容,用于 h 函数和 jsx 语法
│       ├── Modal.vue // 基础组件
│       ├── config.ts // 全局默认配置
│       ├── index.ts // 入口
│       ├── locale // 国际化相关
│       │   ├── index.ts
│       │   └── lang
│       │       ├── en-US.ts
│       │       ├── zh-CN.ts
│       │       └── zh-TW.ts
│       └── modal.type.ts // ts类型声明相关
├── plugins
│   └── modal
│       ├── Content.tsx // 维护 Modal 的内容,用于 h 函数和 jsx 语法
│       ├── Modal.vue // 基础组件
│       ├── config.ts // 全局默认配置
│       ├── index.ts // 入口
│       ├── locale // 国际化相关
│       │   ├── index.ts
│       │   └── lang
│       │       ├── en-US.ts
│       │       ├── zh-CN.ts
│       │       └── zh-TW.ts
│       └── modal.type.ts // ts类型声明相关

因为 Modal 会被 app.use(Modal) 调用作为一个插件,所以都放在plugins目录下

组件内容

首先实现modal.vue的主体显示内容大致如下

html
<Teleport to="body" :disabled="!isTeleport">
  <div v-if="modelValue" class="modal">
    <div class="mask" :style="style" @click="maskClose && !loading && handleCancel()"></div>
    <div class="modal__main">
      <div class="modal__title line line--b">
        <span>{{ title || t("r.title") }}</span>
        <span v-if="close" :title="t('r.close')" class="close" @click="!loading && handleCancel()">
          ✕
        </span>
      </div>
      <div class="modal__content">
        <content v-if="typeof content === 'function'" :render="content" />
        <slot v-else>{{ content }}</slot>
      </div>
      <div class="modal__btns line line--t">
        <button :disabled="loading" @click="handleConfirm">
          <span class="loading" v-if="loading">❍</span>
          {{ t("r.confirm") }}
        </button>
        <button @click="!loading && handleCancel()">{{ t("r.cancel") }}</button>
      </div>
    </div>
  </div>
</Teleport>
<Teleport to="body" :disabled="!isTeleport">
  <div v-if="modelValue" class="modal">
    <div class="mask" :style="style" @click="maskClose && !loading && handleCancel()"></div>
    <div class="modal__main">
      <div class="modal__title line line--b">
        <span>{{ title || t("r.title") }}</span>
        <span v-if="close" :title="t('r.close')" class="close" @click="!loading && handleCancel()">
          ✕
        </span>
      </div>
      <div class="modal__content">
        <content v-if="typeof content === 'function'" :render="content" />
        <slot v-else>{{ content }}</slot>
      </div>
      <div class="modal__btns line line--t">
        <button :disabled="loading" @click="handleConfirm">
          <span class="loading" v-if="loading">❍</span>
          {{ t("r.confirm") }}
        </button>
        <button @click="!loading && handleCancel()">{{ t("r.cancel") }}</button>
      </div>
    </div>
  </div>
</Teleport>

最外层上通过 Vue3 Teleport 内置组件进行包裹,其相当于传送门,将里面的内容传送至body之上

并且从DOM结构上来看,把modal该有的内容(遮罩层、标题、内容、底部按钮)都实现了

关于主体内容

html
<div class="modal__content">
  <content v-if="typeof content==='function'" :render="content" />
  <slot v-else>{{content}}</slot>
</div>
<div class="modal__content">
  <content v-if="typeof content==='function'" :render="content" />
  <slot v-else>{{content}}</slot>
</div>

可以看到根据传入content的类型不同,对应显示不同得到内容

最常见的则是通过调用字符串和默认插槽的形式

js
// 默认插槽
<Modal v-model="show"
       title="演示 slot">
    <div>hello world~</div>
</Modal>

// 字符串
<Modal v-model="show"
       title="演示 content"
       content="hello world~" />
// 默认插槽
<Modal v-model="show"
       title="演示 slot">
    <div>hello world~</div>
</Modal>

// 字符串
<Modal v-model="show"
       title="演示 content"
       content="hello world~" />

通过 API 形式调用Modal组件的时候,content可以使用下面两种

  • h 函数
js
$modal.show({
  title: '演示 h 函数',
  content(h) {
    return h(
      'div',
      {
        style: 'color:red;',
        onClick: ($event: Event) => console.log('clicked', $event.target),
      },
      'hello world ~',
    )
  },
})
$modal.show({
  title: '演示 h 函数',
  content(h) {
    return h(
      'div',
      {
        style: 'color:red;',
        onClick: ($event: Event) => console.log('clicked', $event.target),
      },
      'hello world ~',
    )
  },
})
  • JSX
js
$modal.show({
  title: '演示 jsx 语法',
  content() {
    return (
      <div onClick={($event: Event) => console.log('clicked', $event.target)}>hello world ~</div>
    )
  },
})
$modal.show({
  title: '演示 jsx 语法',
  content() {
    return (
      <div onClick={($event: Event) => console.log('clicked', $event.target)}>hello world ~</div>
    )
  },
})

实现 API 形式

那么组件如何实现API形式调用Modal组件呢?

在Vue2中,我们可以借助Vue实例以及Vue.extend的方式获得组件实例,然后挂载到body上

js
import Modal from './Modal.vue'
const ComponentClass = Vue.extend(Modal)
const instance = new ComponentClass({ el: document.createElement('div') })
document.body.appendChild(instance.$el)
import Modal from './Modal.vue'
const ComponentClass = Vue.extend(Modal)
const instance = new ComponentClass({ el: document.createElement('div') })
document.body.appendChild(instance.$el)

虽然Vue3移除了Vue.extend方法,但可以通过createVNode实现

js
import Modal from './Modal.vue'
const container = document.createElement('div')
const vnode = createVNode(Modal)
render(vnode, container)
const instance = vnode.component
document.body.appendChild(container)
import Modal from './Modal.vue'
const container = document.createElement('div')
const vnode = createVNode(Modal)
render(vnode, container)
const instance = vnode.component
document.body.appendChild(container)

在Vue2中,可以通过this的形式调用全局 API

js
export default {
  install(vue) {
    vue.prototype.$create = create
  },
}
export default {
  install(vue) {
    vue.prototype.$create = create
  },
}

而在 Vue3 的 setup 中已经没有 this概念了,需要调用app.config.globalProperties挂载到全局

js
export default {
  install(app) {
    app.config.globalProperties.$create = create
  },
}
export default {
  install(app) {
    app.config.globalProperties.$create = create
  },
}

事件处理

下面再看看看Modal组件内部是如何处理「确定」「取消」事件的,既然是Vue3,当然采用Compositon API 形式

js
// Modal.vue
setup(props, ctx) {
  let instance = getCurrentInstance(); // 获得当前组件实例
  onBeforeMount(() => {
    instance._hub = {
      'on-cancel': () => {},
      'on-confirm': () => {}
    };
  });

  const handleConfirm = () => {
    ctx.emit('on-confirm');
    instance._hub['on-confirm']();
  };
  const handleCancel = () => {
    ctx.emit('on-cancel');
    ctx.emit('update:modelValue', false);
    instance._hub['on-cancel']();
  };

  return {
    handleConfirm,
    handleCancel
  };
}
// Modal.vue
setup(props, ctx) {
  let instance = getCurrentInstance(); // 获得当前组件实例
  onBeforeMount(() => {
    instance._hub = {
      'on-cancel': () => {},
      'on-confirm': () => {}
    };
  });

  const handleConfirm = () => {
    ctx.emit('on-confirm');
    instance._hub['on-confirm']();
  };
  const handleCancel = () => {
    ctx.emit('on-cancel');
    ctx.emit('update:modelValue', false);
    instance._hub['on-cancel']();
  };

  return {
    handleConfirm,
    handleCancel
  };
}

在上面代码中,可以看得到除了使用传统emit的形式使父组件监听,还可通过_hub属性中添加 on-cancel,on-confirm方法实现在API中进行监听

js
app.config.globalProperties.$modal = {
  show({}) {
    /* 监听 确定、取消 事件 */
  },
}
app.config.globalProperties.$modal = {
  show({}) {
    /* 监听 确定、取消 事件 */
  },
}

下面再来目睹下_hub是如何实现

js
// index.ts
app.config.globalProperties.$modal = {
    show({
        /* 其他选项 */
        onConfirm,
        onCancel
    }) {
        /* ... */

        const { props, _hub } = instance;

        const _closeModal = () => {
            props.modelValue = false;
            container.parentNode!.removeChild(container);
        };
        // 往 _hub 新增事件的具体实现
        Object.assign(_hub, {
            async 'on-confirm'() {
            if (onConfirm) {
                const fn = onConfirm();
                // 当方法返回为 Promise
                if (fn && fn.then) {
                    try {
                        props.loading = true;
                        await fn;
                        props.loading = false;
                        _closeModal();
                    } catch (err) {
                        // 发生错误时,不关闭弹框
                        console.error(err);
                        props.loading = false;
                    }
                } else {
                    _closeModal();
                }
            } else {
                _closeModal();
            }
        },
            'on-cancel'() {
                onCancel && onCancel();
                _closeModal();
            }
    });
}
};
// index.ts
app.config.globalProperties.$modal = {
    show({
        /* 其他选项 */
        onConfirm,
        onCancel
    }) {
        /* ... */

        const { props, _hub } = instance;

        const _closeModal = () => {
            props.modelValue = false;
            container.parentNode!.removeChild(container);
        };
        // 往 _hub 新增事件的具体实现
        Object.assign(_hub, {
            async 'on-confirm'() {
            if (onConfirm) {
                const fn = onConfirm();
                // 当方法返回为 Promise
                if (fn && fn.then) {
                    try {
                        props.loading = true;
                        await fn;
                        props.loading = false;
                        _closeModal();
                    } catch (err) {
                        // 发生错误时,不关闭弹框
                        console.error(err);
                        props.loading = false;
                    }
                } else {
                    _closeModal();
                }
            } else {
                _closeModal();
            }
        },
            'on-cancel'() {
                onCancel && onCancel();
                _closeModal();
            }
    });
}
};