vue2与vue3的差异(总结)?
阅读原文时间:2021年11月19日阅读:1

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

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

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

使用 npm:

# npm 7+,需要加上额外的双短横线
$ npm init vite@latest <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算法优化

    {{ msg }}

被编译成:

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

    {{msg}}

优化前:

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支持的对象操作十分全面

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

    @input="pageTitle = $event" />

    // ChildComponent

    @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

  • 同一节点v-if 比 v-for 优先级更高

  • v-bind="object" 现在排序敏感(绑定相同property,vue2单独的 property 总是会覆盖 object 中的绑定。vue3按顺序决定如何合并)

    // vue2 id="red"
    // vue3 id="blue"

  • 移除 v-on.native 修饰符

    Vue 2 如果想要在一个组件的根元素上直接监听一个原生事件,需要使用v-on 的 .native 修饰符

    Vue3 现在将所有未在组件emits 选项中定义的事件作为原生事件添加到子组件的根元素中(除非子组件选项中设置了 inheritAttrs: false)。

    (强烈建议组件中使用的所有通过emit触发的event都在emits中声明)

    @close="handleComponentEvent" @click="handleNativeClickEvent"/>

    // mycomponent

  • v-for 中的 ref 不再注册 ref 数组

    vue2在 v-for 语句中使用ref属性时,会生成refs数组插入$refs属性中。由于当存在嵌套的v-for时,这种处理方式会变得复杂且低效。

    vue3在 v-for 语句中使用ref属性 将不再会自动在$refs中创建数组。而是,将 ref 绑定到一个 function 中,在 function 中可以灵活处理ref。

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

  • 函数式组件

    在 Vue 2 中,函数式组件有两个主要应用场景:

    作为性能优化,因为它们的初始化速度比有状态组件快得多

    返回多个根节点

    然而Vue 3对有状态组件的性能进行了提升,与函数式组件的性能相差无几。此外,有状态组件现在还包括返回多个根节点的能力。所以,建议只使用有状态组件。

    结合