Vue 3.0汇总

618 阅读2分钟

Vue2迁移变更

提示: Vue3更新了全局API和一些重要的模版指令,贸然的更新Vue2程序,需要花费较高成本来进行兼容;最主要是现有的Vue生态支持3.0的并不多。 官方文档也明确指出:我们仍在开发 Vue 3 的专用迁移版本,该版本的行为与Vue2.x 兼容,运行时 警告不兼容。如果你计划迁移一个非常重要的 Vue 2 应用程序,我们强烈建议你等待迁移版本完成以获得更流畅的体验

全局API

1. createApp

Vue 2 通过 new Vue() 创建的根 Vue 实例,同一个 Vue 实例共享相同的全局配置;全局配置很容易意外地污染其他测试用例。

// 这会影响两个根实例
Vue.mixin({
  /* ... */
})
const app1 = new Vue({ el: '#app-1' })
const app2 = new Vue({ el: '#app-2' })

调用 createApp 返回用户实例

import { createApp } from 'vue'
const app = createApp({})

// 使用生产版提示,仅在使用“dev + full build”才会显示
app.config.productionTip = false

// Vue之外定义的自定义组件
Vue.config.ignoredElements = ['my-el',/^ion-/]
app.config.isCustomElement = tag => tag.startsWith('ion-')

// 卸载应用程序实例的根组件
app.mount("#app")
setTimeout(() => app.unmount('#app'), 5000)

// 基本与Vue2.x相似
app.component('breadcrumb', {})
app.directive('loading',{
  beforeMount(el,binding,vnode){},
  mounted(el,binding,vnode){},
  ...
})
app.provide('guide', 'Vue 3 Guide')
app.mixin()
app.use()
...

2. treeShaking

Vue 2.x 中的这些全局 API 受此更改的影响

  • Vue.nextTick
  • Vue.observable ( Vue.reactive 替代 )
  • Vue.version
  • Vue.compile
  • Vue.set
  • Vue.delete

Vue 2.x写法

// 还能使用
this.$nextTick(()=>{
  /* ... */
})

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

Vue 3.0写法

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

3. 按键修饰符

KeyboardEvent.keyCode被弃用
Vue 2.x写法

// 均不支持
<input @keyup.13="submit" />

Vue.config.keyCodes = { f1: 112 }
<input @keyup.f1="submit" />

Vue 3.0写法

直接将 KeyboardEvent.key暴露的任意有效按键名 转换为 kebab-case 来作为修饰符。

Vue按键别名仍可使用。

<input @keyup.page-up="confirmDelete" />
// 按键别名
<input @keyup.enter="submit" />

4. 事件API

已废除

  • $on
  • $off
  • $once 还存在
  • $emit

5. 过滤器

已废除,建议用计算属性替代

<template>
  <p>{{ create_time | formatTime }}</p>
</template>

<script>
import { formatUTC } from "@/utils/dateTime.js";

export default {
  prop: {
    create_time: {
      type: Number,
      required: true
    }
  },
  filters: {
    formatTime(value) {
      return formatUTC(value)
    }
  }
}
</script>

全局过滤器

<p>{{ $filters.formatTime(create_time) }}</p>

app.config.globalProperties.$filters = {
  formatTime(value) {
    return formatUTC(value)
  }
}

6. 内联模板 Attribute

已废除

<my-component inline-template>
  <div>
    <p>它们被编译为组件自己的模板</p>
    <p>不是父级所包含的内容。</p>
  </div>
</my-component>

默认slot

<!-- 2.x 语法 -->
<my-comp inline-template :msg="parentMsg">
  {{ msg }} {{ childState }}
</my-comp>

<!-- 默认 Slot 版本 -->
<my-comp v-slot="{ childState }">
  {{ parentMsg }} {{ childState }}
</my-comp>

模版指令

1. v-model

减少开发者使用 v-model 指令时混淆,增加v-model参数,更具有灵活性。
Vue2.0绝大多数的 v-model 需要更改为 v-model:value Vue 2.x写法

// v-model 等同于绑定了 value的props和input事件
<ChildComponent v-model="pageTitle" />
// 等同于
<ChildComponent :value="pageTitle" @input="pageTitle = $event" />

// sync 对props进行双向绑定
<ChildComponent :title.sync="pageTitle" />
this.$emit('update:title', newValue)

Vue 3.0写法

// 添加参数区分 v-model 绑定的值
<ChildComponent v-model:value="pageTitle" v-model:content ="pageContent" />
// 等同于
<ChildComponent
    :value="pageTitle"
    @update:value="pageTitle = $event"
    :content="pageContent"
    @update:content="pageContent = $event"
/>

// 支持修饰符
<ChildComponent v-model:title.trim="pageTitle" />

2. key

v-if、v-else、v-else-if节点上,会自动生成唯一的key ;
若手动提供key,每个分支必须使用唯一的key ;
template节点上使用v-for的key Vue 2.x写法

<template v-for="item in list">
  <div :key="item.id">...</div>
  <span :key="item.id">...</span>
</template>

Vue 3.0写法

<template v-for="item in list" :key="item.id">
  <div>...</div>
  <span>...</span>
</template>

3. v-if与v-for的优先级

在Vue 2.x,节点需要同时使用v-if和v-for的情况下,v-for会优先作用;
3.0 变更了优先顺序

<div v-if="boolean" v-for="item in list" :key="item.id">
   /* ... */
</div>

4. v-bind

Vue 2.x中,定义了v-bind="object" 和一个单独的property, 单独的property总是会覆盖object中的绑定;
3.0 而会根据绑定顺序来决定覆盖;

<div id="red" v-bind="{ id: 'blue' }">
    /* ... */
</div>

// 2.x  props: red
// 3.0  props: blue

5. v-for中的ref

Vue 2.x中,v-for上的ref会自动创建数组;
3.0中 ref 只会在最后一个节点上;要绑定多个 ref 请将 ref 绑定到一个更灵活的函数上( 3.0 新特性 )

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

函数式组件

functional被移除;更多详细信息

渲染函数

1. render

Vue 2.x写法

export default {
  render(h) {
    return h('div')
 }}

Vue 3.0写法

import { h } from 'vue'
export default {
  render() {
    return h('div')
}}

vue-router中 component 使用该方法并不起作用?

import { h } from 'vue'

component: { render: () => h("router-view") }

2. slot统一

移除 this.scopedSlots改用this.scopedSlots 改用 this.slots

3. 自定义指令

生命周期

Vue.directive('highlight', {
  beforeMount(el, binding, vnode) {},
  mounted(el, binding, vnode) {}
  ...
})
  • bind → beforeMount
  • inserted → mounted
  • beforeUpdate
  • update → 移除
  • componentUpdated → updated
  • beforeUnmount
  • unbind → unmounted

4. 过渡&动画

class变更

  • v-enter → v-enter-from
  • v-leave → v-leave-from

其他改动

  • data始终需要是一个函数
data(){
  return {
  }
}
  • /deep/ 建议改成 ::v-deep,VueCli build会产生警告
/deep/.i-icon{}
::v-deep(.i-icon){}
  • template 中允许存在多个元素
<template>
  <div>1</div>
  <div>2</div>
</template>

对比Vue 2.x

  • 对TypeScript支持友好
  • 对虚拟DOM进行了重写,对模板的编译进行了优化操作
  • 更方便支持jsx
  • CompositionAPI,受ReactHook启发
  • 性能提升
  • 体积变小 关于性能是怎样提升的,引入掘金另一位大佬的博客喔

组合式API

Vue2.x的组织逻辑在大多数情况下都有效;然而,当我们的组件变得更大时,逻辑关注点的列表也会增长,尤其是对于那些刚开始就没有编写过这些组件的人来说,不能有效的看到每一块逻辑层面。 每种颜色都代表一个逻辑关注点,需要根据data的值上下反复查阅 一句话来说,组合式API 做到的就是: 将与同一个逻辑关注点相关的代码配置在一起
这种写法不是绝对的!vue2.x的options api依旧可以使用!

setup

整合所有逻辑写在setup内,将渲染上下文需要的数据通过return暴露出来

<template>
  <div>
    <input type="text" v-model="newItem" />
    <button @click="addItem(newItem)">新增</button>
  </div>
  <div>
    <h3>您的代办事项:</h3>
    <h3>共 {{ count }} 项</h3>
    <ul>
      <li v-for="(item, index) in list" :key="index">
        <span>{{ item }}</span>
        <span style="color: red; margin-left: 20px" @click="removeItem(index)"
          >X</span
        >
      </li>
    </ul>
  </div>
</template>

<script>
import { computed, ref } from "vue";
export default {
  name: "todo",
  setup() {
    const newItem = ref("");
    const list = ref([]);
    const addItem = (newItem) => {
      list.value.push(newItem);
    };
    const removeItem = (index) => {
      list.value.splice(index, 1);
    };
    const count = computed(() => list.value.length);
    return {
      newItem,
      list,
      addItem,
      removeItem,
      count,
    };
  },
};
</script>

生命周期

之前的生命周期之前加上“on”
因为 setup 是围绕 beforeCreate和 created 来运行的,所以不需要定义他们,逻辑应该直接在setup中编写;从视角看来,setup 在 beforeCreate 之前就被调用;所以使用"this",是拿不到任何实例引用

<script>
import { onBeforeMount, onMounted ... } from "vue";

export default {
  setup() {
    onBeforeMount(()=> {})    
    onMounted(() => {})     
    onBeforeUpdate(()=> {})    
    onUpdated(() => {})     
    onBeforeUnmount(()=> {})    
    onUnmounted(() => {})
    onErrorCaptured(()=> {})
    return {}
  }
}
</script>

参数

setup接受两个参数,props,context
props不多介绍了,值得一提的是,它是响应式的,当传入新的props,它将被更新
context是一个普通的对象,它暴露三个组件的property,可被解构 { attrs, slots, emit }

export default {
  props: {
    title: String
  },
  setup(props, context) {
    console.log(props.title)
    // Attribute (非响应式对象)
    console.log(context.attrs)
    // 插槽 (非响应式对象)
    console.log(context.slots)
    // 触发事件 (方法)
    console.log(context.emit)
  }
}

模板引用refs

由于setup不能通过this获取实例引用,refs 获取真实dom元素的用法有所改变,这个和react的用法一样,为了获得对模板内元素或组件实例的引用,需要像往常一样在setup()中声明一个ref并返回它

<template>
    <div ref="node"></div> 
</template>

<script>
import { ref } from 'vue'; 
export default {  
   setup() {
      const node = ref(null);
      // console.log(node)
      return { node };
   }
}; 
</script>

在v-for中

<template>
  <div v-for="(item, i) in list" :ref="el => { if (el) divs[i] = el }">
    {{ item }}
  </div>
</template>

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

  export default {
    setup() {
      const list = reactive([1, 2, 3])
      const divs = ref([])

      // 确保在每次更新之前重置ref
      onBeforeUpdate(() => {
        divs.value = []
      })

      return {
        list,
        divs
      }
    }
  }
</script>

响应式系统

Vue2.x 是通过 Object.defineProperty结合订阅/发布模式实现的。
而 Vue3.0 则是采用 ES6 的 Proxy 代理来拦截对目标对象的访问。

以下内容是在组合式API Setup()内执行的

创建

1. reactive

reactive 相当于 Vue 2.x 中的 Vue.observable() API;
它接收一个普通对象然后返回该普通对象的响应式代理(Proxy);
响应式转换是“深层”的,会影响对象内部所有嵌套的属性。基于 ES2015 的 Proxy 实现,返回的代理对象不等于原始对象。建议仅使用代理对象而避免依赖原始对象;

import { reactive } from 'vue'
// 响应式状态
const state = reactive({
  count: 0
})

2. ref

接受一个参数值并返回一个响应式且可改变的 ref 对象。ref 对象拥有一个指向内部值的单一属性value

import { ref } from 'vue'

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

ref在渲染上下文时,会自动展开内部值,不需要再追加 .value

<template>
  <div>
    <span>{{ count }}</span>
  </div>
</template>
<script>
  import { ref } from 'vue'
  export default {
    setup() {
      const count = ref(0)
      return {
        count
      }
    }
  }</script>

被reactive object嵌套访问更改,同样会展开内部值;

const count = ref(0)
const state = reactive({
  count
})
console.log(state.count) // 0
state.count = 1
console.log(count.value) // 1


const books = reactive([ref('Vue 3 Guide')])
// 这里需要 .value
console.log(books[0].value)

3. 解构reactive

es6语法解构获取我们想要的property,在reactive中无法作用

import { reactive } from 'vue'

const book = reactive({
  author: 'Vue',
  title: 'Vue 3 Guide',
})

let { author, title } = book
// console.log(author)  丢失

使用 toRefs 来转换为ref,这些 ref 将保留与源对象的响应式关联

import { toRefs } from 'vue'
let { author, title } = toRefs(book)

4. readonly

防止更改reactive

import { reactive, readonly } from 'vue'

const original = reactive({ count: 0 })
const copy = readonly(original)

// 转换copy 将导失败并导致警告
copy.count++ 
// 警告: "Set operation on key 'count' failed: target is readonly."

计算

computed,用法与vue2.0基本一致

import { computed } from 'vue'

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


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

plusOne.value = 1
console.log(count.value) // 0

监听

1. watchEffect

为了根据反应状态自动应用和重新应用副作用,我们可以使用 watchEffect 方法。它立即执行传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数。

Vue2.x写法

computed: {
    routeTitle() {
      return this.$route.meta.title;
    },
    breadcrumb() {
      return this.$route.matched.filter(data => data.meta.breadcrumb);
    },
    openKeys() {
      return this.$route.meta.nav.parent ? [this.$route.meta.nav.parent] : []
    }
 }

Vue3.0写法

let routeTitle = ref("");
let openKeys = ref([]);
let breadcrumb = ref([]);

watchEffect(() => {
  routeTitle.value = route.meta.title;
  openKeys.value = route.meta.nav.parent ? [route.meta.nav.parent] : [];
  breadcrumb.value = route.matched.filter(data => data.meta.breadcrumb);
});

停止侦听

const stop = watchEffect(() => {})
stop()

2. watch

watch是惰性的,即只有当被侦听的源发生变化时才执行回调,更具体地说明什么状态应该触发侦听器重新运行。

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

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

其他响应性API

官方文档

  1. isProxy 检查对象是 reactive 还是 readonly创建的代理

  2. isReactive 检查对象是否是 reactive创建的响应式 proxy。

import { reactive, isReactive } from 'vue'
export default {
  setup() {
    const state = reactive({
      name: 'John'
    })
    console.log(isReactive(state)) // -> true
  }
}
  1. isReadonly 检查对象是否是由readonly创建的只读代理

  2. toRaw 返回 reactive 或 readonly 代理的原始对象。这是一个转义口,可用于临时读取而不会引起代理访问/跟踪开销,也可用于写入而不会触发更改。

  3. markRaw 标记一个对象,使其永远不会转换为代理。返回对象本身。

const foo = markRaw({})
console.log(isReactive(reactive(foo))) // false
// 嵌套在其他响应式对象中时也可以使用
const bar = reactive({ foo })
console.log(isReactive(bar.foo)) // false
  1. unref 如果参数为 ref,则返回内部值,否则返回参数本身。这是 val = isRef(val) ? val.value : val

  2. toRef 可以用来为源响应式对象上的 property 性创建一个 ref。然后可以将 ref 传递出去,从而保持对其源 property 的响应式连接。

  3. toRefs 将响应式对象转换为普通对象,其中结果对象的每个 property 都是指向原始对象相应 property 的ref。

  4. isRef 检查对象是否为ref创建对象

生态系统

Vue-router 4.0

1. 兼容性改动

createRouter

无需调用new Router(),而是调用createRouter

import { createRouter } from 'vue-router'
const router = createRouter({
  // ...
})

mode

  • history → createWebHistory()
  • hash → createWebHashHistory()
  • abstract → createMemoryHistory()
import { createRouter, createWebHistory } from 'vue-router'

createRouter({
  history: createWebHistory(),
  routes: [],
})

更多兼容性改动

2. 结合组合式API

setup内部使用路由,routerrouter和 route 依然存在,模版内仍可使用

  • useRouter
  • useRoute
import { useRouter, useRoute } from "vue-router";

export default {
    setup(){
        //相当于 this.$router
        const router = useRouter();
        let handleLinkTo = name => {
          router.push({ name });
        };
        //相当于 this.$route
        const route = useRoute();
        let id = route.params.id;
    
        return {
            handleRouter,
            id
        }
    }
}

导航卫士

  • onBeforeRouteLeave
  • onBeforeRouteUpdate
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
export default {
  setup() {
    onBeforeRouteLeave((to, from) => {
      const answer = window.confirm(
        'Do you really want to leave? you have unsaved changes!'
      )
      if (!answer) return false
    })

    const userData = ref()

    onBeforeRouteUpdate(async (to, from) => {
      if (to.params.id !== from.params.id) {
        userData.value = await fetchUser(to.params.id)
      }
    })
  }
 }

useLink

Vue Router将RouterLink的内部行为公开为Composition API函数

import { RouterLink, useLink } from 'vue-router'
export default {
  name: 'AppLink',

  props: {
    ...RouterLink.props,
    inactiveClass: String,
  },

  setup(props) {
    const { route, href, isActive, isExactActive, navigate } = useLink(props)

    const isExternalLink = computed(
      () => typeof props.to === 'string' && props.to.startsWith('http')
    )

    return { isExternalLink, href, navigate, isActive }
  },}

Vite

  • 快速的冷启动
  • 即时的模块热更新
  • 真正的按需编译 代码仓库