vue2与vue3的差异(总结)?

vue作者尤雨溪在开发 vue3.0 的时候开发的一个基于浏览器原生 ES imports 的开发服务器(开发构建工具)。那么我们先来了解一下vite

Vite

Vite,一个基于浏览器原生 ES imports 的开发服务器。利用浏览器去解析 imports,在服务器端按需编译返回,完全跳过了打包这个概念,服务器随起随用。同时不仅有 Vue 文件支持,还搞定了热更新,而且热更新的速度不会随着模块增多而变慢。针对生产环境则可以把同一份代码用 rollup 打。虽然现在还比较粗糙,但这个方向我觉得是有潜力的,做得好可以彻底解决改一行代码等半天热更新的问题。它做到了本地快速开发启动, 用 vite 文档上的介绍,它具有以下特点:

  • 快速的冷启动,不需要等待打包操作;
  • 即时的热模块更新,替换性能和模块数量的解耦让更新飞起;
  • 真正的按需编译,不再等待整个应用编译完成;

使用 npm:

# npm 7+,需要加上额外的双短横线
$ npm init [email protected] <project-name> -- --template vue

$ cd <project-name>
$ npm install
$ npm run dev

或者 yarn:

$ yarn create vite <project-name> --template vue
$ cd <project-name>
$ yarn
$ yarn dev

概览

在这里插入图片描述

  • 速度更快
  • 体积减少
  • 更易维护
  • 更接近原生
  • 更易使用
  1. 重写了虚拟Dom实现
    diff算法优化
<div>
  <span/>
  <span>{{ msg }}</span>
</div>

被编译成:

import { createVNode as _createVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createBlock as _createBlock } from "vue"

export function render(_ctx, _cache) {
  return (_openBlock(), _createBlock("div", null, [
    _createVNode("span", null, "static"),
    _createVNode("span", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)
  ]))
}

首先静态节点进行提升,会提升到 render 函数外面,这样一来,这个静态节点永远只被创建一次,之后直接在 render 函数中使用就行了。
Vue在运行时会生成number(大于0)值的PatchFlag,用作标记,仅带有PatchFlag标记的节点会被真正追踪,无论层级嵌套多深,它的动态节点都直接与Block根节点绑定,无需再去遍历静态节点,所以处理的数据量减少,性能得到很大的提升。
在这里插入图片描述

  1. 事件监听缓存:cacheHandlers
<div>
  <span @click="onClick">
    {{msg}}
  </span>
</div>

优化前:

import { toDisplayString as _toDisplayString, createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock } from "vue"

export function render(_ctx, _cache) {
  return (_openBlock(), _createBlock("div", null, [
    _createVNode("span", { onClick: _ctx.onClick }, _toDisplayString(_ctx.msg), 9 /* TEXT, PROPS */, ["onClick"])
  ]))
}

onClick会被视为PROPS动态绑定,后续替换点击事件时需要进行更新。
优化后:

import { toDisplayString as _toDisplayString, createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock } from "vue"

export function render(_ctx, _cache) {
  return (_openBlock(), _createBlock("div", null, [
    _createVNode("span", {
      onClick: _cache[1] || (_cache[1] = $event => (_ctx.onClick($event)))
    }, _toDisplayString(_ctx.msg), 1 /* TEXT */)
  ]))
}

会自动生成一个内联函数,这个内联函数里面再去引用当前组件最新的onclick,然后把这个内联函数cache起来,第一次渲染的时候会创建内联函数并且缓存,后续的更新就直接从缓存里面读同一个函数,既然是同一个函数就没有再更新的必要,就变成了一个静态节点
3. SSR速度提高
当有大量静态的内容时,这些内容会被当做纯字符串推进一个buffer里面,即使存在动态的绑定,会通过模板 插值嵌入进去,这样会比通过虚拟dom来渲染的快很多。vue3.0 当静态文件大到一定量的时候,会用_ceratStaticVNode方法在客户端去生成一个static node, 这些静态node,会被直接innerHtml,就不需要创建对象,然后根据对象渲染

  1. tree-shaking
    在这里插入图片描述

tree-shakinng 原理
主要依赖es6的模块化的语法,es6模块依赖关系是确定的,和运行时的状态无关,可以进行可靠的静态分析,
分析程序流,判断哪些变量未被使用、引用,进而删除对应代码
前提是所有的东西都必须用ES6 module的import来写

按照作者的原话解释,Tree-shaking其实就是:把无用的模块进行“剪枝”,很多没有用到的API就不会打包到最后的包里
在Vue2中,全局 API 如 Vue.nextTick() 是不支持 tree-shake 的,不管它们实际是否被使用,都会被包含在最终的打包产物中。
而Vue3源码引入tree shaking特性,将全局 API 进行分块。如果你不使用其某些功能,它们将不会包含在你的基础包中
5. compositon Api
在这里插入图片描述

没有Composition API之前vue相关业务的代码需要配置到option的特定的区域,中小型项目是没有问题的,但是在大型项目中会导致后期的维护性比较复杂,同时代码可复用性不高
compositon api提供了以下几个函数:

  • setup (入口函数,接收两个参数(props,context))

  • ref (将一个原始数据类型转换成一个带有响应式特性)

  • reactive (reactive 用来定义响应式的对象)

  • watchEffect

  • watch

  • computed

  • toRefs (解构响应式对象数据)

  • 生命周期的hooks

    如果用ref处理对象或数组,内部会自动将对象/数组转换为reactive的代理对象
    ref内部:通过给value属性添加getter/setter来实现对数据的劫持
    reactive内部:通过使用proxy来实现对对象内部所有数据的劫持,并通过Reflect反射操作对象内部数据
    ref的数据操作:在js中使用ref对象.value获取数据,在模板中可直接使用

import { useRouter } from 'vue-router'
import { reactive, onMounted, toRefs } from 'vue'

// setup在beforeCreate 钩子之前被调用
// setup() 内部,this是undefined,因为 setup() 是在解析其它组件选项之前被调用的,所以 setup() 内部的 this 的行为与其它选项中的 this 完全不同。这在和其它选项式 API 一起使用 setup() 时可能会导致混淆
// props 是响应式的,当传入新的 prop 时,它将被更新(因为props是响应式的,所以不能使用 ES6 解构,因为它会消除 prop 的响应性。)

// props参数:包含组件props配置声明且传入了的所有props的对象
// attrs参数:包含没有在props配置中声明的属性对象,相当于this.$attrs
// slots参数:包含所有传入的插槽内容的对象,相当于this.$slots
// emit参数:可以用来分发一个自定义事件,相当于this.$emit
setup (props, {attrs, slots, emit}) {
  const state = reactive({
    userInfo: {}
  })

  const getUserInfo = async () => {
    state.userInfo = await GET_USER_INFO(props.id)
  }

  onMounted(getUserInfo) // 在 `mounted` 时调用 `getUserInfo`

// setup的返回值

// 一般都是返回一个对象,为模板提供数据,就是模板中可以直接使用此对象中所有属性/方法
// 返回对象中的属性会与data函数返回对象的属性合并成为组件对象的属性
// 返回对象中的方法会与methods中的方法合并成组件对象的方法
// 若有重名,setup优先
  return {
    ...toRefs(state),
    getUserInfo
  }
}

灵活的逻辑组合与复用
可与现有的Options API一起使用
与选项API最大的区别的是逻辑的关注点
选项API这种碎片化使得理解和维护复杂组件变得困难,在处理单个逻辑关注点时,我们必须不断地上下翻找相关代码的选项块。
compositon API将同一个逻辑关注点相关代码收集在一起
6. Fragment(碎片)
在这里插入图片描述

<template>
  <header>...</header>
  <main v-bind="$attrs">...</main>
  <footer>...</footer>
</template>

Vue 3不再限于模板中的单个根节点,它正式支持了多根节点的组件,可纯文字,多节点,v-for等
render 函数也可以返回数组
7. Teleport(传送门)
在这里插入图片描述

这个组件的作用主要用来将模板内的 DOM 元素移动到其他位置。
允许我们控制在 DOM 中哪个父节点下渲染了 HTML

<teleport to="body">
  <div v-if="modalOpen" class="modal">
    <div>
      I'm a teleported modal!
      (My parent is "body")
      <button @click="modalOpen = false">
        Close
      </button>
    </div>
  </div>
</teleport>
  1. 更好的Typescript支持
    vue3是基于typescipt编写的,可以享受到自动的类型定义提示

  2. 自定义渲染 API
    在这里插入图片描述

    vue官方实现的 createApp 会给我们的 template 映射生成 html 代码,但是要是你不想渲染生成到 html ,而是要渲染生成到 canvas 之类的不是html的代码的时候,那就需要用到 Custom Renderer API 来定义自己的 render 渲染生成函数了。
    意味着以后可以通过 vue, Dom 编程的方式来进行canvas、webgl 编程
    默认的目标渲染平台
    在这里插入图片描述

    自定义目标渲染平台
    在这里插入图片描述

  3. 响应原理的变化
    vue2对象响应化:遍历每个key,通过 Object.defineProperty API定义getter,setter 进而触发一些视图更新
    数组响应化:覆盖数组的原型方法,增加通知变更的逻辑
    vue2响应式痛点
    递归,消耗大
    新增/删除属性,需要额外实现单独的API
    数组,需要额外实现
    Map Set Class等数据类型,无法响应式
    修改语法有限制
    vue3响应式方案: 使用ES6的Proxy进行数据响应化,解决上述vue2所有痛点,Proxy可以在目标对象上加一层拦截/代理,外界对目标对象的操作,都会经过这层拦截。Proxy可以在目标对象上加一层拦截/代理,外界对目标对象的操作,都会经过这层拦截,相比 Object.defineProperty ,Proxy支持的对象操作十分全面

一, 全局api

1. 全局 Vue API 已更改为使用应用程序实例

vue2使用全局api 如 Vue.component, Vue.mixin, Vue.use等,缺点是会导致所创建的根实例将共享相同的全局配置(从相同的 Vue 构造函数创建的每个根实例都共享同一套全局环境。这样就导致一个问题,只要某一个根实例对 全局 API 和 全局配置做了变动,就会影响由相同 Vue 构造函数创建的其他根实例。)
vue3 新增了createApp,调用createApp返回一个应用实例,拥有全局API的一个子集,任何全局改变 Vue 行为的 API 现在都会移动到应用实例上
在这里插入图片描述

2. 组件挂载

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

const app = createApp(App)
app.mount('#app')

createApp初始化后会返回一个app对象,里面包含一个mount函数
mount函数是被重写过的

  1. 处理传入的容器并生成节点;
  2. 判断传入的组件是不是函数组件,组件里有没有render函数,template属性,没有就用容器的innerHTML作为组件的template;
  3. 清空容器内容
  4. 运行缓存的mount函数实现挂载组件;

二, 模板指令

  • 组件上 v-model 用法更改,替换 v-bind.sync
    vue2默认会利用名为 value 的 prop 和名为 input 的事件
// ParentComponent
<ChildComponent v-model="pageTitle" />

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

<ChildComponent :value="pageTitle" @input="pageTitle = $event" />

 // ChildComponent

<input type="text" :value="value" @input="$emit('input', $event.target.value)">

如果想要更改 prop 或事件名称,则需要在组件中添加 model 选项:
model选项,允许组件自定义用于 v-model 的 prop 和事件

// ChildComponent
<input type="text" :value="title" @input="$emit('change', $event.target.value)">
export default {
  model: {
    prop: 'title',
    event: 'change'
  },
  props: {
    title: String
  }
}

使用 title 代替 value 作为 model 的 prop

vue2.3 新增.sync (对某一个 prop 进行“双向绑定”,是update:title 事件的简写)

// ParentComponent
<ChildComponent :title.sync="name" />

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

<ChildComponent :title="pageTitle" @update:title="pageTitle = $event" />
 // ChildComponent
 <input type="text" :value="title" @input="$emit('update:title', $event.target.value)">

在 3.x 中,自定义组件上的 v-model 相当于传递了 modelValue prop 并接收抛出的 update:modelValue 事件
prop:value -> modelValue;
event:input -> update:modelValue
v-bind 的 .sync 修饰符和组件的 model 选项已移除,可用 v-model加参数 作为代替
vue3 可以将一个 argument 传递给 v-model:
<ChildComponent v-model:title="pageTitle" />
等价于
<ChildComponent :title="pageTitle" @update:title="pageTitle = $event" />
可使用多个model
在这里插入图片描述

  • 可以在template元素上添加 key
<template v-for="item in list" :key="item.id">
  <div>...</div>
</template>
  • 同一节点v-if 比 v-for 优先级更高
  • v-bind=”object” 现在排序敏感(绑定相同property,vue2单独的 property 总是会覆盖 object 中的绑定。vue3按顺序决定如何合并)
<div id="red" v-bind="{ id: 'blue' }" ></div>
// vue2 id="red"
// vue3 id="blue"
  • 移除 v-on.native 修饰符
    Vue 2 如果想要在一个组件的根元素上直接监听一个原生事件,需要使用v-on 的 .native 修饰符
    Vue3 现在将所有未在组件emits 选项中定义的事件作为原生事件添加到子组件的根元素中(除非子组件选项中设置了 inheritAttrs: false)。
    (强烈建议组件中使用的所有通过emit触发的event都在emits中声明)
<my-component @close="handleComponentEvent" @click="handleNativeClickEvent"/>
// mycomponent
<template>
  <div>
    <button @click="$emit('click')">click</button>
    <button @click="$emit('close')">close</button>
  </div>
</template>
<script>
  export default {
    emits: ['close']
  }
</script>
  • v-for 中的 ref 不再注册 ref 数组
    vue2在 v-for 语句中使用ref属性时,会生成refs数组插入$refs属性中。由于当存在嵌套的v-for时,这种处理方式会变得复杂且低效。
    vue3在 v-for 语句中使用ref属性 将不再会自动在$refs中创建数组。而是,将 ref 绑定到一个 function 中,在 function 中可以灵活处理ref。
<div v-for="item in list" :ref="setItemRef"></div>

export default {
 setup() {
   let itemRefs = []
   const setItemRef = el => {
     itemRefs.push(el)
   }
   return {
     setItemRef
   }
 }
}

三, 组件

  • 函数式组件
    在 Vue 2 中,函数式组件有两个主要应用场景:
    作为性能优化,因为它们的初始化速度比有状态组件快得多
    返回多个根节点
    然而Vue 3对有状态组件的性能进行了提升,与函数式组件的性能相差无几。此外,有状态组件现在还包括返回多个根节点的能力。所以,建议只使用有状态组件。

    结合<template>的函数式组件:

  1. functional 移除
  2. 将 props 的所有引用重命名为 $props,attrs 重命名为 $attrs。
<template>
  <component :is=`h${$props.level}`  v-bind='$attrs' />
</template>

<script>
  export default {
    props: ['level']
  }
</script>

函数写法:
相较于 Vue 2.x 有三点变化:

  1. 所有的函数式组件都是用普通函数创建的,换句话说,不需要定义 { functional: true } 组件选项。
  2. export default导出的是一个函数,函数有两个参数:
    props
    context(上下文):context是一个对象,包含attrs、slot、emit属性
  3. h函数需要全局导入
import { h } from 'vue'

const DynamicHeading = (props, context) => {
  return h(`h${props.level}`, context.attrs, context.slots)
}

DynamicHeading.props = ['level']

export default DynamicHeading
  • 异步组件需要 defineAsyncComponent 方法来创建
    异步组件的导入需要使用辅助函数defineAsyncComponent来进行显式声明
import { defineAsyncComponent } from 'vue'
const child = defineAsyncComponent(() => import('@/components/async-component-child.vue'))

带选项异步组件,component 选项重命名为 loader

const asyncPageWithOptions  = defineAsyncComponent({
  loader: () => import('./NextPage.vue'),
  delay: 200,
  timeout: 3000,
  error: ErrorComponent,
  loading: LoadingComponent
})
  • (新增)组件事件需要在 emits 选项中声明()
    强烈建议使用 emits 记录每个组件所触发的所有事件。
    因为移除了 v-on.native 修饰符。任何未声明 emits 的事件监听器都会被算入组件的 $attrs 并绑定在组件的根节点上。
    如果emit的是原生的事件(如,click),就会存在两次触发。
    一次来自于$emit的触发;
    一次来自于根元素原生事件监听器的触发;
    (emits 1.更好的记录已发出的事件,2.验证抛出的事件)
 export default {
    props: ['text'],
    emits: ['accepted']
  }
emits: {
    click: null,
    submit: payload => {
      if (payload.email && payload.password) {
        return true
      } else {
        console.warn(`Invalid submit event payload!`)
        return false
      }
    }
  }

四, 渲染函数

  • 渲染函数API
    h是全局导入,而不是作为参数传递给渲染函数
    在 2.x 中,render 函数会自动接收 h 函数作为参数
    在 3.x 中,h 函数需要全局导入。由于 render 函数不再接收任何参数,它将主要在 setup() 函数内部使用。可以访问在作用域中声明的响应式状态和函数,以及传递给 setup() 的参数
import { h, reactive } from 'vue'

export default {
  setup(props, { slots, attrs, emit }) {
    const state = reactive({
      count: 0
    })

    function increment() {
      state.count++
    }

    // 返回render函数
    return () =>
      h(
        'div',
        {
          onClick: increment
        },
        state.count
      )
  }
}
  • 移除$listeners整合到 $attrs
    包含了父作用域中的(不含emits的) v-on 事件监听器。它可以通过 v-on=”$listeners” 传入内部组件
{{$attrs}}
<grand-son v-bind="$attrs"></grand-son>
  • $attrs包含class&style
    在vue2中,关于父组件使用子组件有这样一个原则:

默认情况下父作用域的不被认作 props 的 attribute 绑定 (attribute bindings) 将会“回退”且作为普通的 HTML attribute 应用在子组件的根元素上

这句话的意思是,父组件调用子组件时,给子组件锚点标签添加的属性中,除了在子组件的props中声明的属性,其他属性会自动添加到子组件根元素上。
为此,vue添加了inheritAttrs = false,这些默认行为将会被去掉,通过实例 property $attrs 可以让这些 attribute 生效,且可以通过 v-bind 显性的绑定到非根元素上。

五, 自定义元素

  • 自定义元素检测在编译时执行
    自定义元素交互
    Vue 2中,通过 Vue.config.ignoredElements 配置自定义元素
Vue.config.ignoredElements = ['plastic-button']

Vue 3 通过app.config.isCustomElement

const app = Vue.createApp({})
app.config.isCustomElement = tag => tag === 'plastic-button'
  • Vue 3.x 对 is做了新的限制
    当在 Vue 保留的 component标签上使用is时,它的行为将与 Vue 2.x 中的一致
    当在不同组件标签上使用is时,is会被当做一个不同的prop;
    当在普通的 HTML 元素上使用is,is将会被当做元素的属性。
    新增了v-is,专门来实现在普通的 HTML 元素渲染组件。

六, 其他

  • destroyed 生命周期选项被重命名为 unmounted
  • beforeDestroy 生命周期选项被重命名为 beforeUnmount
    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-awWIzouv-1637307558259)(assets/vue3/img.png)]
    整体来看其实变化不大,使用setup代替了之前的beforeCreate和created,其他生命周期名字有些变化,功能都是没有变化的
  • Props 的默认值函数不能访问this
    替代方案:
    把组件接收到的原始 prop 作为参数传递给默认函数;
    inject API 可以在默认函数中使用。
import { inject } from 'vue'

export default {
  props: {
    theme: {
      default (props) {
        // `props` 是传递给组件的原始值。
        // 也可以使用 `inject` 来访问注入的属性
        return inject('theme', 'default-theme')
      }
    }
  }
}
  • 自定义指令 API 与组件生命周期一致
const MyDirective = {
  created(el, binding, vnode, prevVnode) {}, // 新增
  beforeMount() {},
  mounted() {},
  beforeUpdate() {}, // 新增
  updated() {},
  beforeUnmount() {}, // 新增
  unmounted() {}
}

绑定组件的实例从 Vue 2.x 的vnode.context移到了binding.instance中

  • data 选项应始终被声明为一个函数
    data 组件选项声明不再接收 js 对象,只接受函数形式的声明。
<script>
  import { createApp } from 'vue'

  createApp({
    data() {
      return {
        apiKey: 'a1b2c3'
      }
    }
  }).mount('#app')
</script>

当合并来自 mixin 或 extend 的多个 data 返回值时,data现在变为浅拷贝形式(只合并根级属性)。

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

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

vue2

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

vue3

{
  "user": {
    "id": 2
  }
}
  • 过渡的 class 名更改(过渡类名 v-enter 修改为 v-enter-from、过渡类名 v-leave 修改为 v-leave-from。)
  • transition-group 不再需要设置根元素( 不再默认渲染根元素,但仍可以使用 tag prop创建一个根元素。)
  • 侦听数组(当侦听一个数组时,只有当数组被替换时才会触发回调。如果你需要在数组改变时触发回调,必须指定 deep 选项。)
  • 已挂载的应用不会取代它所挂载的元素(在vue2中,当挂载一个具有 template 的应用时,被渲染的内容会替换我们要挂载的目标元素。在 Vue 3.x 中,被渲染的应用会作为子元素插入,从而替换目标元素的 innerHTML)
  • 生命周期 hook: 事件前缀改为 vnode-(监听子组件和第三方组件的生命周期)

移除API

  • 不再支持使用数字 (即键码) 作为 v-on 修饰符,vue3建议使用按键alias(别名)作为v-on的修饰符。
<input v-on:keyup.delete="confirmDelete" />
  • vue3将移除且不再支持 filters,如果需要实现过滤功能,建议通过method或computed属性来实现(如果需要使用全局过滤器vue3提供了globalProperties。我们可以借助globalProperties来注册全局过滤, 全局过滤器里面定义的只能是method。)
const app = createApp(App)

app.config.globalProperties.$filters = {
  currencyUSD(value) {
    return '$' + value
  }
}
<template>
  <p>{{ $filters.currencyUSD(accountBalance) }}</p>
</template>
  • 内联模板 (inline-template attribute移除)
  • $children(如果需要访问子组件实例,建议使用 $refs)
  • propsData 选项之前用于在创建 Vue 实例的过程中传入 prop,现在它被移除了。如果想为 Vue 3 应用的根组件传入 prop,使用 createApp 的第二个参数。
  • 全局函数 set 和 delete 以及实例方法 $set 和 $delete。基于代理的变化检测不再需要它们了。

用于迁移的构建版本

@vue/compat (即“迁移构建版本”) 是一个 Vue 3 的构建版本,提供了可配置的兼容 Vue 2 的行为。

该构建版本默认运行在 Vue 2 的模式下——大部分公有 API 的行为和 Vue 2 一致,仅有一小部分例外。使用在 Vue 3 中发生改变或被废弃的特性时会抛出运行时警告。一个特性的兼容性也可以基于单个组件进行开启或禁用。

已知的限制:

  • 基于vue2内部API或文档中未记载行为的依赖。最常见的情况就是使用 VNodes 上的私有 property。如果你的项目依赖诸如 Vuetify、Quasar 或 Element UI 等组件库,那么最好等待一下它们的 Vue 3 兼容版本。

  • 对IE11的支持:Vue 3 已经官方放弃对 IE11 的支持。如果仍然需要支持 IE11 或更低版本,那你仍需继续使用 Vue 2。

  • 服务端渲染:该迁移构建版本可以被用于服务端渲染,但是迁移一个自定义的服务端渲染设置有更多工作要做。大致的思路是将 vue-server-renderer 替换为 @vue/server-renderer。Vue 3 不再提供一个包渲染器,推荐使用 Vite 以支持 Vue 3 服务端渲染。

Tags: