聊聊vue2与vue3的差异

1,834 阅读10分钟

前言

vue3 已经出来一段时间了,刚发布的时候我们还可以用让子弹飞一会来给自己一个理由先缓一缓。现在有不少公司面试都要求有 vue3 的项目经验,vue2 与3 的差异也是面试中常问的问题。是时候好好去了解学习一下,使用在我们的项目中了,你准备好了吗。

vue2与vue3的对比

1. 响应式原理

vue2 是通过数据劫持结合发布者订阅者模式,通过 Object.definerProperty 对各个属性进行 getter 和 setter ,当数据变化时,发布消息给依赖收集器,通知观察者进行相应的回调,从而实现视图更新。主要是分为几个模块,Observer , Compile 和 Watcher 。Observer 进行数据监听,Compile来进行模板解析,Watcher为这两者的沟通的桥梁。这三个模块以MVVM为入口实现了整个响应式。

vue3 是通过 es6 的 proxy 对整个对象进行劫持从而实现了响应式。

上面这些话也只是我背过的简单的面试八股文,我会在后面的文章对 vue2 和 vue3 的响应式做详细的分析

2. api

1 vue2 采用 Options API(选项式api) vue3采用 Composition API (组合式api)

选项式 api 顾名思义就是提供了所有 api 让你来直接选择。vue2 将定义的属性,方法都绑定在 this 上,你直接按照他的模板在对应的地方做定义和使用,不过他是有比较明确的使用场景。就如我们的数据需要定义在 data 函数里面,方法写在 methods 里面。我们看下官网的解释

使用选项式 API,我们可以用包含多个选项的对象来描述组件的逻辑,例如 datamethods 和 mounted。选项所定义的属性都会暴露在函数内部的 this 上,它会指向当前的组件实例。

组合式 api 就像是乐高。有无数个小模型你自己选择使用,不同的组合就能做成不同的作品,更加随意,但对你的要求也就更高。同样的我们看下官网的说法

通过组合式 API,我们可以使用导入的 API 函数来描述组件逻辑。

<template>
  <div>
    <span>{{ count }}</span>
    <button @click="increment">加1</button>
  </div>
</template>

<script setup>
import { ref } from 'vue'
const count = ref(0)
const increment = () => {
  count.value++
}
</script>

比如这个组件中 我们定义的响应式数据 count 等同于我们在 vue2 data 内定义一个count 数据 onMounted等同于 vue2 中的 mounted。推荐使用 <script setup> 这种语法糖模式。书写方便,代码也更加简洁。如果是使用 vue2 那种再写入 setup 函数,我们定义的数据和方法就需要暴露出去才可以使用,我们来看下写法。

<template>
  <div >
    <span>{{ count }}</span>
    <button @click="increment">加1</button>
  </div>
</template>
<script>
export default {
  setup() {
    const count = ref(0)
    const increment = () => {
      count.value++
    }
    return {
      count,
      increment
    }
  }
}
</script>

但是在vue3中 <script setup> 模式中和 <script> 中使用 setup 函数中有些一点差别。比如我想调用子组件方法,需要在子组件中用defineExpose({xxx})将方法暴露出来。(xxx为我们定义的方法)。其他的差异暂时还没遇到,遇到的话会及时更新过来。

我们来看下官网上的一个 vue2 与 vue3 写法的对比。 在 vue2 中如果实现一个动态更改数据的功能,需要在data 里面存放数据,在 methods 里面定义方法,如果需要我们还需要在生命周期中添加处理。相当于东市买骏马,西市买鞍鞯。在 vue3 中一块的逻辑放一个地方写时方便,用时也方便。不过习惯用 vue2 写法的小伙伴一时半会可能不习惯这种写法,做个项目就好了,会发现挺香的。 image.png

还有一点是更好的逻辑复用,我们来看一个官网的例子
如果我们要直接在组件中使用组合式 API 实现鼠标跟踪功能,它会是这样的: 这个是以组件形式来做的一个逻辑封装

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

const x = ref(0)
const y = ref(0)

function update(event) {
  x.value = event.pageX
  y.value = event.pageY
}

onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
</script>

<template>Mouse position is at: {{ x }}, {{ y }}</template>

我们可以把这个逻辑以一个组合式函数的形式提取到外部文件中: 这样就可以减少组件的进入实现同样的功能

// mouse.js
import { ref, onMounted, onUnmounted } from 'vue'

// 按照惯例,组合式函数名以“use”开头
export function useMouse() {
  // 被组合式函数封装和管理的状态
  const x = ref(0)
  const y = ref(0)

  // 组合式函数可以随时更改其状态。
  function update(event) {
    x.value = event.pageX
    y.value = event.pageY
  }

  // 一个组合式函数也可以挂靠在所属组件的生命周期
  // 来启动和卸载副作用
  onMounted(() => window.addEventListener('mousemove', update))
  onUnmounted(() => window.removeEventListener('mousemove', update))

  // 通过返回值暴露所管理的状态
  return { x, y }
}

2 响应式数据的定义

在 vue2 中是在 data 函数中定义响应式数据

*vue2 中*
data() {
      return {
        count:0,
        user:{
          name:'李白',
          price:18888
        }
     }
 }
* vue3 中*
 
 <script  setup>
 import {ref,reactive} from "vue"
  const count=ref(0)
  count user=reactive({
    name:'李白',
    price:18888
  })
</script>

如果在 script 中使用count值需要使用count.value (ref生成响应式数据格式),模板数据中不需要加 .value直接使用 {{count}} 即可。reactive 生成的数据可以按照 vue2 中那样使用。但是需要注意的是不能对 user 直接进行赋值 如 user={name:'妲己',price:'1888'},不然就会失去响应式。如果必须对 user 直接赋值可以使用ref定义,或者定义一个对象,使对象属性为user,使这个对象为响应式,就可以用obj.user=xxx

image.png

image.png

组件传参

常用的几种组件传参 props emit ref 实例 vuex (envetbus $parent 和 $children 个人不是很常用) 之类 props emit ref 在 vue3 中都需要去进行定义接收,我们来简单看个例子

ref 的定义与 vue2 有点差别,需要进行定义,需要注意的一点,子组件使用 setup 语法糖模式是 需要用 defineExpose 将想要给父组件使用的方法暴露出来。

*父组件*
<template>
  <div>
     <div>父组件</div>
     <button @click="setChildrenVal">修改子组件的值</button>
     <br>
      <appChildren :name="parentName" ref="appChildrenRef" @change-parent-props="parentCallback"></appChildren>
  </div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import appChildren from './app-children.vue'; //setup 语法糖模式无需再注册,可以直接使用
   const parentName=ref('我是父组件传过来的值')
   const appChildrenRef=ref()// vue2 中 this.$refs.appChildrenRef.xxxx()
   //调用子组件方法更新数据
   const setChildrenVal=()=>{
    appChildrenRef.value.setVal('我是来修改子组件数据的')
   }
   const parentCallback=(value:string)=>{
      parentName.value=value
   }
</script>
*子组件*
<template>
    <div>
       <div>我是子组件 初始值为 <span style="color: blue;">{{ initVal }}</span></div>
       <div><span style="color: red;">{{ name }}</span></div>
       <div>接收父组件的值,重新定义使用  {{propsName}}</div>
       <button @click="setParentProps">修改父组件传过来的值</button>
    </div>
  </template>
  <script setup lang="ts">
  import { ref, watch } from 'vue';
     const props=defineProps({
        name:{
            type:String,
            default:'子组件初始值'
        }
     })
     const initVal=ref('我是子组件初始化数据')

     //提供方法给父组件使用
     const setVal=(value:string)=>{
        initVal.value=value
     }
     defineExpose({setVal}) //使用 script setup 语法糖模式需要将子组件暴露出来,父组件才能够使用

     //更新父组件传过来的值
     const emit=defineEmits(['changeParentProps'])
     const setParentProps=()=>{
      emit('changeParentProps','我是子组件修改的数据')
     }

     //接收父组件的值,如果需要对数据修改,自己使用的,可以进行重新定义接收,配合watch 对父组件数据同步更新
     const propsName=ref(props.name)
     watch(()=>props.name,(newVal=>{
      propsName.value=newVal
     }))
  </script>
computed watch

我们来简单看下实现,整体的使用思想和 vue2 一样,只是书写方式有点不同。vue3 watch 中比较方便的一点是可以同时监听多个值,用数组形式传入,同样也会以数组形式返回出来

 <template>
  <div>
     商品{{ goods.name }}
     <p>数量{{ goods.number }}</p>
     <p>单价{{ goods.unit }}</p>
     总价{{ price }}
     <button @click="setGoods()">送出20台,剩余降价999</button>
  </div>
</template>
<script setup lang="ts">
import { reactive,computed, watch} from 'vue';
const goods=reactive({
  name:'xiaomi',
  number:100,
  unit:1999
})
const price=computed(()=>{
  return goods.number*goods.unit
})
const setGoods=()=>{
  goods.number-=20
  goods.unit-=999
}
watch(goods,(newVal=>{console.log(newVal)}),{deep:true,immediate:true}) //直接监听对象
watch(()=>goods.unit,(newVal=>{console.log(newVal)}))  //监听单个 监听对象的属性需要使用函数方式
watch([()=>goods.unit,()=>goods.number],(newVal=>{console.log(newVal)}))  //监听多个
</script>
vue2
 <script>
export default{
   data(){
    return{
      goods:{
        name:'xiaomi',
        number:100,
        unit:1999
      }
    }
   },
   computed:{
    price(){
      return this.goods.number*this.goods.unit
    }
   },
   methods:{
    setGoods(){
      this.goods.number-=20
      this.goods.unit-=999
    }
   },
   watch:{
      goods:{
        handler(newVal,oldVal){
          console.log(newVal)
           console.log(oldVal)
        },
        deep:true,
        mmediate:true
      }
   }
}
</script>
生命周期

生命周期的话差别不是很大,只是写法上有点小区别,在setup 函数内其他生命周期都是以回调函数方式使用

image.png

<script setup lang="ts">
   console.log('setup')
   onBeforeMount(()=>{
      console.log("onBeforeMount")
   })
   onMounted(()=>{
      console.log("onMounted")
   })
   ...
</script>
支持多节点

在vue2中vue组件只能有一个根节点, vue3中对此已经不做限制,可以根据自己实际需求来使用

异步组件

defineAsyncComponent 在大型项目中,我们可能需要拆分应用为更小的块,并仅在需要时再从服务器加载相关组件,我们来看下代码就很好理解

<template>
  <div>
     <div>父组件</div>
     <button @click="showChildren">显示异步组件</button>
     <div v-if="show">
      <appChildren ></appChildren>
     </div>
  </div>
</template>
<script setup lang="ts">
import { ref ,defineAsyncComponent} from 'vue';
// 第一种
import appChildren from './app-children.vue';  
const show=ref(false)

//第二种
// const appChildren = defineAsyncComponent(() =>
//   import('./app-children.vue'),
// )
//第二种 添加 加载与错误状态配置
// const appChildren = defineAsyncComponent({
//   // 加载函数
//   loader: () => import('./app-children.vue'),

//   // 加载异步组件时使用的组件
// //   loadingComponent: LoadingComponent,
//   // 展示加载组件前的延迟时间,默认为 200ms
//   delay: 200,

//   // 加载失败后展示的组件
// //   errorComponent: ErrorComponent,
//   // 如果提供了一个 timeout 时间限制,并超时了
//   // 也会显示这里配置的报错组件,默认值是:Infinity
//   timeout: 3000
// })

const showChildren=()=>{
   show.value=true
}
</script>

我们引入一个子组件,分别用两种方式引入看下有什么不同,第一种用平常的方式进行引入,会发现无论我们使不使用,该组件都会发起网路请求进行加载,而且打包时还会和一起进行打包。第二种的话会在你使用时再进行网络请求,打包时还会单独成一个模块包,对首页加载有一定的优化作用。当然,异步加载可能会出现加载缓慢或者加载失败的问题,所以我们有了第二种添加加载和错误状态的配置。 image.png

image.png

组件自定义v-model,插槽

有兴趣的小伙伴可以看下我之前文章对这个两个点的个人理解 juejin.cn/post/720556…
juejin.cn/post/720709…

3. 相关优化 diff算法,打包等

  1. vue3 源码使用 ts 来写的,vue3 相对于 vue2 对 ts 更加契合。
  2. vue2、vue3 的 diff 算法实现差异主要体现在:处理完首尾节点后,对剩余节点的处理方式。 (具体细节我会在后面文章进行整理分析)
  • vue2 是通过对旧节点列表建立一个 { key, oldVnode } 的映射表,然后遍历新节点列表的剩余节点,根据newVnode.key在旧映射表中寻找可复用的节点,然后打补丁并且移动到正确的位置。
  • vue3 则是建立一个存储新节点数组中的剩余节点在旧节点数组上的索引的映射关系数组,建立完成这个数组后也即找到了可复用的节点,然后通过这个数组计算得到最长递增子序列,这个序列中的节点保持不动,然后将新节点数组中的剩余节点移动到正确的位置
  1. Tree shaking  是一种通过清除多余代码方式来优化项目打包体积的技术,专业术语叫 Dead code elimination 简单来说就是去除不需要的代码,不影响正常功能。
    在 vue2 中每个属性和方法都是挂载在当前组件的 this 上面,捆绑程序无法检测到哪些属性在代码中被使用到,所以就无法进行多余的剔除;Vue3源码引入tree shaking特性,将全局 API 进行分块。如果不使用其某些功能,它将不会包含在基础包中。通过Tree shakingVue3给我们带来的好处是:
  • 减少程序体积(更小)
  • 减少程序执行时间(更快)
  • 便于将来对程序架构进行优化(更友好)

最后

以上这些就是个人对 vue2 和 3 的简单的对比了解,当然这些只是常用的一部分,如果后面有用到其他的,也会同步更新。一些比较重要的点,比如响应式原理,源码,diff算法等会在后面文章做详细的分析。本人也在学习中,如有不对的地方请不吝赐教。我是南岸月明,致力于用最通俗易懂的话来聊明白一个个知识点,希望在帮助自己的同时对你也有所帮助!