vue2 -> vue3重要差异一览

1,270 阅读5分钟

vue3 正式推出至今,对其更新了解一直比较零散, 于是打算完整的了解一下,

然后其中比较重要的内容整理成了这份文档,

该文档只做精简整理,不做深入

proxy

为了更好了拦截对象/数组的变化, vue的双向绑定底层实现也是放弃了 object.defineProperty 选择了 proxy

object.defineProperty 的弊端就是无法监听对象的新增字段,vue初始化的时候还需要花费很多时间去递归对对象的每个属性挂载, 所以导致了 vue2 的各种限制需要使用 $set 取设置新增的, 并且需要取 hack 掉数组的pushreplace等等方法,所以 vue3 也是选择了更好用的 proxy

全局方法

// Vue 2.x
Vue.prototype.$http = () => {}

// Vue 3
const app = createApp({})
app.config.globalProperties.$http = () => {}

composition api

原本 vue2 中对象上的各种属性在 vue3 中被抽离出来,通过 composition api 形式提供开发者使用,因此能更好的支持更好的 tree sharking,即打包时未使用的api 不会被打入

ref

包装基本类型使用, 当然也能传入对象(内部调用 reactive)

修改ref的值需要通过 .value

import { ref } from 'vue'
const count = ref(2)
// 修改
count.value = 3

reactive

用于引用类型, 和 ref 不同的是不需要通过 .value

import { reactive } from 'vue'
const person = reactive({ name: '张三', age: 18 })
// 修改
person.name = '李四'

isRef

判断某个值是否是 ref 创建出来的

import { ref, isRef } from 'vue'
const count = ref(2)

isRef(count) // true

toRef

把 reactive 的某个值都处理成 ref, 会保持对其原本 reactive 的响应式连接

const state = reactive({
  foo: 1,
  bar: 2
})

const fooRef = toRef(state, 'foo')
fooRef.value++
console.log(state.foo) // 2

state.foo++
console.log(fooRef.value) // 3

toRefs

把 reactive 的每个值都处理成 ref, 多是为了更方便的在模版中使用 reactive中的值时使用

export default {
  setup() {
    // 可以在不失去响应性的情况下解构
    const state = reactive({
      foo: 1,
      bar: 2
    })

    return {
      ...toRefs(state)
    }
  }
}

isReactive

看的出来, 判断是否是 reactive

readonly

接收一个 ref 或者 reactive, 返回一个只读的响应式新对象

const original = reactive({ count: 0 })

const copy = readonly(original)
copy.count++ // 警告!

watchEffect

立即执行传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数。

const count = ref(0)

watchEffect(() => console.log(count.value))
// -> logs 0

setTimeout(() => {
  count.value++
  // -> logs 1
}, 100)

watch

与 watchEffect 相比,watch 允许我们:

  • 惰性地执行副作用;
  • 更具体地说明应触发侦听器重新运行的状态;
  • 访问被侦听状态的先前值和当前值。
// 官网 demo

// 侦听一个 getter
const state = reactive({ count: 0 })
watch(() => state.count, (count, prevCount) => {
    /* ... */
  }
)

// 直接侦听一个 ref
const count = ref(0)
watch(count, (count, prevCount) => {
  /* ... */
})

// 侦听多个源
watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => {
  /* ... */
})

computed

const count = ref(1)
const plusOne = computed(() => count.value + 1)

// 自定义 get set 
const plusOne = computed({
  get: () => count.value + 1,
  set: val => {
    count.value = val - 1
  }
})

filter

不再支持过滤器, 建议用方法调用或计算属性替换

// 官网 demo

<template>
  <p>{{ accountInUSD }}</p>
</template>

<script>
  export default {
    props: {
      accountBalance: {
        type: Number,
        required: true
      }
    },
    computed: {
      accountInUSD() {
        return '$' + this.accountBalance
      }
    }
  }
</script>

当然也可以使用 js 原生的管道运算符

// 相当于是 plus(123)
<template>
  <p>{{  123 |> plus }}</p>
</template>

<script>
  export default {
    setup() {
      function plus(val) {
        return val + 100
      } 
      return {
        myFormat
      }
    }
  }
</script>

生命周期

  • beforeCreate -> setup
  • created -> setup
  • beforeMount -> onBeforeMount
  • mounted -> onMounted
  • beforeUpdate -> onBeforeUpdate
  • updated -> onUpdated
  • beforeUnmount -> onBeforeUnmount
  • unmounted -> onUnmounted
  • errorCaptured -> onErrorCaptured

teleport

传送门, 将该组件内部的dom 渲染到指定的容器内。

使用场景:modal 弹窗终于不用在组件内部弹出来了

to 参数接收一个指定 dom 的 selector, 并支持多个同时使用,他会追加进去,一起出现

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

<teleport to="#modal" >
  <div>a</div>
</teleport>
<teleport to="#modal" >
  <div>b</div>
</teleport>

// 结果
<div id="modal">
  <div>a</div>
  <div>b</div>
</div>

css 深度选择器

:deep()

<style scoped>
.a :deep(.b) {
  /* ... */
}
</style>

上面的代码会被编译成:

.a[data-v-f3f3eg9] .b {
  /* ... */
}

css v-bind

单文件组件的 <style> 标签可以通过 v-bind 这一 CSS 函数将 CSS 的值关联到动态的组件状态上:

<template>
  <div class="text">hello</div>
</template>

<script>
export default {
  data() {
    return {
      color: 'red'
    }
  }
}
</script>

<style>
.text {
  color: v-bind(color);
}
</style>

这个语法同样也适用于 <script setup>,且支持 JavaScript 表达式 (需要用引号包裹起来)

<script setup>
const theme = {
  color: 'red'
}
</script>

<template>
  <p>hello</p>
</template>

<style scoped>
p {
  color: v-bind('theme.color');
}
</style>

style module

<style module> 标签会被编译为 CSS Modules 并且将生成的 CSS 类作为 $style 对象的键暴露给组件:

<template>
  <p :class="$style.red">
    This should be red
  </p>
</template>

<style module>
.red {
  color: red;
}
</style>

Ref 数组

在 Vue 2 中,在 v-for 中使用的 ref attribute 会用 ref 数组填充相应的 $refs property。当存在嵌套的 v-for 时,这种行为会变得不明确且效率低下。

在 Vue 3 中,此类用法将不再自动创建 $ref 数组。要从单个绑定获取多个 ref,请将 ref 绑定到一个更灵活的函数上 (这是一个新特性):

<div v-for="item in list" :ref="setItemRef"></div>

export default {
  data() {
    return {
      itemRefs: []
    }
  },
  methods: {
    setItemRef(el) {
      if (el) {
        this.itemRefs.push(el)
      }
    }
  },
  beforeUpdate() {
    this.itemRefs = []
  },
  updated() {
    console.log(this.itemRefs)
  }
}

结合组合式 API:

import { onBeforeUpdate, onUpdated } from 'vue'

export default {
  setup() {
    let itemRefs = []
    const setItemRef = el => {
      if (el) {
        itemRefs.push(el)
      }
    }
    onBeforeUpdate(() => {
      itemRefs = []
    })
    onUpdated(() => {
      console.log(itemRefs)
    })
    return {
      setItemRef
    }
  }
}

fragment

允许多个根节点

<template>
  <a>...</a>
  <b>...</b>
  <c>...</c>
</template>

data 选项

vue2: data 可以是 object 或者是 function

vue3: 已标准化为只接受返回 object 的 function

<script>
export default {
  data() {
    return {
      color: 'red'
    }
  }
}
</script>

Mixin 合并行为变更

当来自组件的 data() 及其 mixin 或 extends 基类被合并时,合并操作现在将被浅层次地执行:

const Mixin = {
  data() {
    return {
      user: {
        name: 'Jack',
        id: 1
      }
    }
  }
}

const CompA = {
  mixins: [Mixin],
  data() {
    return {
      user: {
        id: 2
      }
    }
  }
}

在 Vue 2.x 中,生成的 $data 是:

{
  "user": {
    "id": 2,
    "name": "Jack"
  }
}

在 3.0 中,其结果将会是:

{
  "user": {
    "id": 2
  }
}

移除$listeners

在 Vue 3 中,事件监听器被认为是只是以 on 为前缀的 attribute,这样它就成为了 attrs对象的一部分,因此attrs 对象的一部分,因此 listeners 被移除了。

emits

需要使用 emits 记录每个组件所触发的所有事件。 vue3 中移除了 .native 修饰符。任何未在 emits 中声明的事件监听器都会被算入组件的 $attrs,并将默认绑定到组件的根节点上。

<template>
  <div>
    <p>{{ text }}</p>
    <button v-on:click="$emit('accepted')">OK</button>
  </div>
</template>
<script>
  export default {
    props: ['text'],
    emits: ['accepted']
  }
</script>

v-on.native

对于子组件中未被定义为组件触发的所有事件监听器,Vue 现在将把它们作为原生事件监听器添加到子组件的根元素中 (除非在子组件的选项中设置了 inheritAttrs: false)。

拿click 事件举例,现在想要 @click.native 效果, 子组件的 emits 里不写 'click' 就行

v-model

支持多个 v-model,可自定义props 值接收

// 父组件
<ChildComponent v-model:title="pageTitle" v-model:content="pageContent" />

<!-- 是以下的简写: -->

<ChildComponent
  :title="pageTitle"
  @update:title="pageTitle = $event"
  :content="pageContent"
  @update:content="pageContent = $event"
/>
// 子组件
// ChildComponent.vue

export default {
  props: {
    modelValue: String 
  },
  emits: ['update:modelValue'],
  methods: {
    changePageTitle(title) {
      this.$emit('update:modelValue', title) // 以前是 `this.$emit('input', title)`
    }
  }
}

总结:

接收: 直接props 定义就行, 比如: name、 age

提交: update: 开头, emit('update:name', value), emit('update:age', value)

外部使用: v-model:name="xxx" v-model:age="aaa"

事件 API

onon,off 和 $once 实例方法已被移除,组件实例不再实现事件触发接口。

自定义指令

目标事件和生命周期统一

created - 新增!在元素的 attribute 或事件监听器被应用之前调用。

bind → beforeMount

inserted → mounted

beforeUpdate:新增!在元素本身被更新之前调用,与组件的生命周期钩子十分相似。

update → 移除!该钩子与 updated 有太多相似之处,因此它是多余的。请改用 updated。

componentUpdated → updated

beforeUnmount:新增!与组件的生命周期钩子类似,它将在元素被卸载之前调用。

unbind -> unmounted

异步组件

在 Vue 3 中,由于函数式组件被定义为纯函数,因此异步组件需要通过将其包裹在新的 defineAsyncComponent 助手方法中来显式地定义:

import { defineAsyncComponent } from 'vue'
import ErrorComponent from './components/ErrorComponent.vue'
import LoadingComponent from './components/LoadingComponent.vue'

// 不带选项的异步组件
const asyncModal = defineAsyncComponent(() => import('./Modal.vue'))

// 带选项的异步组件
const asyncModalWithOptions = defineAsyncComponent({
  loader: () => import('./Modal.vue'),
  delay: 200,
  timeout: 3000,
  errorComponent: ErrorComponent,
  loadingComponent: LoadingComponent
})

Suspense

组件内的异步组件处理方案

default 插槽里的节点会尽可能展示出来。如果不能,则展示 fallback 插槽里的节点。

<template>
  <suspense>
    <template #default>
      <todo-list />
    </template>
    <template #fallback>
      <div>
        Loading...
      </div>
    </template>
  </suspense>
</template>

<script>
export default {
  components: {
    TodoList: defineAsyncComponent(() => import('./TodoList.vue'))
  }
}
</script>

default 插槽放你原本的组件

fallback 插槽放组件未加载好时的 loading

渲染函数 API

vue2 里面是在 render(h){} 函数里接收的 h 函数, 当我们需要复用方法时需要将 h 传来传去, 很不方便

vue3 h 函数被抽离到 'vue' 包里面了, render 函数内不再接收

import { h, reactive } from 'vue'

v-bind 合并行为

在 2.x 中,如果一个元素同时定义了 v-bind="object" 和一个相同的独立 attribute,那么这个独立 attribute 总是会覆盖 object 中的绑定。

<!-- 模板 -->
<div id="red" v-bind="{ id: 'blue' }"></div>
<!-- 结果 -->
<div id="red"></div>

在 3.x 中,如果一个元素同时定义了 v-bind="object" 和一个相同的独立 attribute,那么绑定的声明顺序将决定它们如何被合并。换句话说,相对于假设开发者总是希望独立 attribute 覆盖 object 中定义的内容,现在开发者能够对自己所希望的合并行为做更好的控制。

<!-- 模板 -->
<div id="red" v-bind="{ id: 'blue' }"></div>
<!-- 结果 -->
<div id="blue"></div>

<!-- 模板 -->
<div v-bind="{ id: 'blue' }" id="red"></div>
<!-- 结果 -->
<div id="red"></div>

VNode 生命周期事件

// vue2
<template>
  <child-component @hook:updated="onUpdated">
</template>
// vue3
<template>
  <child-component @vnode-updated="onUpdated">
</template>

// 驼峰
<template>
  <child-component @vnodeUpdated="onUpdated">
</template>

生命周期事件监听方式,从 @hook: 开头改为 @vnode- 开头

过渡的 class 名更改

vue2

.v-enter,
.v-leave-to {
  opacity: 0;
}

.v-leave,
.v-enter-to {
  opacity: 1;
}

vue3 重命名为

.v-enter-from,
.v-leave-to {
  opacity: 0;
}

.v-leave-from,
.v-enter-to {
  opacity: 1;
}

在 prop 的默认函数中访问

生成 prop 默认值的工厂函数不再能访问 this。

取而代之的是:

组件接收到的原始 prop 将作为参数传递给默认函数;

inject API 可以在默认函数中使用。

import { inject } from 'vue'

export default {
  props: {
    theme: {
      default (props) {
        // `props` 是传递给组件的、
        // 在任何类型/默认强制转换之前的原始值,
        // 也可以使用 `inject` 来访问注入的 property
        return inject('theme', 'default-theme')
      }
    }
  }
}

侦听数组

当使用 watch 选项侦听数组时,只有在数组被替换时才会触发回调。换句话说,在数组被改变时侦听回调将不再被触发。要想在数组被改变时触发侦听回调,必须指定 deep 选项。

{
  watch: {
    bookList: {
      handler(val, oldVal) {
        console.log('book list changed')
      },
      deep: true
    }
  }
}