vue2和vue3 实现数据双向绑定的原理详解,vue2和vue3 组件传值详解,vue2和vue3的生命周周期中都进行了什么事情详解?

大白话 vue2和vue3 实现数据双向绑定的原理详解,vue2和vue3 组件传值详解,vue2和vue3的生命周周期中都进行了什么事情详解

引言

你盯着屏幕上的vue项目,眉头紧锁。测试发来消息:“在vue3项目里用v-model绑定表单,输入框输入内容,页面居然没反应?”你心里咯噔一下,这不是刚从vue2迁移过来吗?怎么连双向绑定都出问题了。再一看,子组件接收父组件的值用的还是props,可数据就是传不过去,控制台还没报错。更让人崩溃的是,生命周期钩子明明写对了,数据却总是获取不到,就像拳头打在了棉花上,有劲使不出。

作为前端开发,vue框架是我们的“老朋友”了。但从vue2切换到vue3,不少人都经历过这样的“阵痛”:习惯了Object.defineProperty,突然换成Proxy有点不知所措;用惯了beforeDestroy,现在变成onBeforeUnmount总记不住;父子组件传值时,不知道什么时候该用v-model,什么时候该用emit。

别以为这些差异只是“换个写法”那么简单。当产品经理催着上线,你却因为没搞懂vue3的双向绑定原理而卡壳;当用户反馈页面数据更新延迟,你才发现是生命周期钩子用错了时机;当团队协作时,有人用vue2的写法,有人用vue3的语法,代码一团糟。

这篇文章不会像官方文档那样罗列API,而是用前端人都懂的大白话,告诉你为什么vue3的双向绑定性能更好,为什么组件传值方式变了,为什么生命周期钩子要改名。就像布洛芬能缓解头痛一样,这篇指南能让你在面对vue2和vue3的差异时,理清思路,高效解决问题。

问题场景

场景1:“双向绑定失灵”的尴尬

小李刚把项目从vue2升到vue3,用v-model绑定表单输入框,结果输入内容后,页面上的数据纹丝不动。他检查了data里的变量,也确认了v-model的写法,可就是找不到问题所在。最后才发现,vue3里的响应式数据定义方式变了,他还在用vue2的data函数写法。

痛点:双向绑定是vue的核心特性之一,可vue2和vue3实现方式不同,稍不注意就会失灵。就像你习惯了用钥匙开门,突然换成了密码锁,明明门就在眼前,却进不去。

场景2:“组件传值迷路”的困惑

小王在vue3项目中,父组件给子组件传值,子组件用props接收,可始终拿不到数据。他怀疑是props名称写错了,反复核对后还是没问题。后来才知道,vue3中如果父组件传的是动态数据,需要用不同的方式处理,而他还用着vue2的老办法。

痛点:组件传值是项目开发中最常用的功能,vue2和vue3在传值方式和处理上有不少差异。就像寄快递,以前填个地址就行,现在还要填收件人的身份证号,少了一步就寄不出去。

场景3:“生命周期钩子失效”的无奈

小张在vue3项目中,想在组件加载完成后获取数据,他用了vue2中的mounted钩子,结果数据获取总是延迟。排查后发现,vue3的生命周期钩子虽然功能类似,但执行时机和写法有变化,他没及时调整。

痛点:生命周期钩子控制着组件从创建到销毁的整个过程,用错了不仅会导致功能异常,还可能影响性能。就像做饭,火候没掌握好,要么没熟,要么糊了。

场景4:“团队协作混乱”的烦恼

一个团队里,有人习惯vue2的Options API,有人喜欢vue3的Composition API,写出来的代码风格迥异。小陈接手一个别人写的vue3项目,里面混合了两种写法,他花了半天时间才理清楚逻辑,严重影响了开发效率。

痛点:vue3推出了Composition API,和vue2的Options API差异较大,团队成员如果对两种写法掌握不透彻,很容易导致代码混乱,增加维护成本。就像一个团队里有人说中文,有人说英文,沟通起来费时费力。

技术原理

数据双向绑定原理

vue2的双向绑定:Object.defineProperty的“功劳”

vue2采用数据劫持结合发布-订阅模式的方式实现双向绑定,核心是Object.defineProperty()方法。

它会遍历data中的所有属性,为每个属性添加getter和setter。当属性被访问时,触发getter,收集依赖;当属性被修改时,触发setter,通知依赖更新。

同时,vue的编译过程会解析模板中的指令,如v-model,生成对应的Watcher,建立视图和数据之间的联系。当数据变化时,Watcher会触发更新函数,更新视图;当视图变化时,会通过事件监听修改数据,从而实现双向绑定。

vue3的双向绑定:Proxy的“升级”

vue3改用Proxy来实现响应式数据。Proxy可以创建一个对象的代理,从而实现对目标对象的属性读取、修改等操作的拦截。

相比Object.defineProperty,Proxy有以下优势:

  • 可以监听对象的新增属性和删除属性,而Object.defineProperty只能监听已存在的属性。
  • 可以监听数组的变化,不需要像vue2那样重写数组方法。
  • 拦截的是整个对象,而不是单个属性,效率更高。

vue3中,通过reactive函数将对象转为响应式对象,通过ref函数处理基本类型数据。当访问或修改响应式对象的属性时,Proxy会拦截这些操作,触发相应的依赖收集和更新。

组件传值原理

vue2的组件传值方式
  1. 父传子:props
    父组件通过在子组件标签上添加属性,子组件通过props选项接收。props是单向绑定的,子组件不能直接修改props的值,需要通过$emit触发父组件的事件来修改。

  2. **子传父:emit∗∗子组件通过emit** 子组件通过emit子组件通过emit方法触发自定义事件,父组件在子组件标签上监听该事件,并在事件处理函数中获取子组件传递的数据。

  3. 兄弟传值:事件总线(EventBus)
    通过创建一个空的vue实例作为事件总线,兄弟组件之间通过on监听事件,on监听事件,on监听事件,emit触发事件来传递数据。

  4. 跨级传值:provide/inject
    父组件通过provide提供数据,子组件或孙组件通过inject注入数据,实现跨级组件通信。

  5. 全局状态管理:vuex
    通过vuex存储全局状态,组件可以通过dispatch或commit方法修改状态,通过mapState等辅助函数获取状态。

vue3的组件传值方式
  1. 父传子:props
    基本用法和vue2类似,但vue3中props的定义方式更灵活,可以使用数组、对象或setup函数中的props参数。

  2. 子传父:emit
    vue3中,子组件通过emit函数触发自定义事件,父组件在子组件标签上监听该事件。和vue2不同的是,vue3中需要在emits选项中声明自定义事件,提高代码的可维护性。

  3. 兄弟传值:mitt
    vue3中移除了事件总线,推荐使用mitt库来实现兄弟组件传值,用法和EventBus类似。

  4. 跨级传值:provide/inject
    用法和vue2类似,但vue3中provide和inject可以在setup函数中使用,并且支持响应式数据。

  5. 全局状态管理:pinia
    pinia是vue3推荐的状态管理库,相比vuex,它简化了API,支持TypeScript,更符合vue3的Composition API风格。

生命周期原理

vue2的生命周期

vue2的生命周期可以分为四个阶段:创建、挂载、更新、销毁,每个阶段都有对应的钩子函数。

  1. 创建阶段
  • beforeCreate:实例初始化后,数据观测和事件配置之前被调用,此时data和methods都未初始化。
  • created:实例创建完成后被调用,此时data和methods已经初始化,但DOM还未生成,$el属性还不存在。
  1. 挂载阶段
  • beforeMount:在挂载开始之前被调用,此时模板已经编译完成,但还未挂载到DOM上。
  • mounted:挂载完成后被调用,此时DOM已经生成,$el属性存在,可以进行DOM操作。
  1. 更新阶段
  • beforeUpdate:数据更新时被调用,此时数据已经更新,但DOM还未重新渲染。
  • updated:DOM重新渲染完成后被调用,可以在该钩子中执行依赖于DOM的操作。
  1. 销毁阶段
  • beforeDestroy:实例销毁之前被调用,此时实例还可以使用,可以在这里清除定时器、解绑事件等。
  • destroyed:实例销毁后被调用,此时实例的所有指令都已解绑,事件监听器被移除,子实例也被销毁。
vue3的生命周期

vue3的生命周期钩子函数名称和vue2类似,但有一些变化,并且可以在setup函数中使用。

  1. 创建阶段
  • setup:相当于vue2的beforeCreate和created的组合,在实例初始化时被调用,此时可以进行数据初始化等操作。
  1. 挂载阶段
  • onBeforeMount:和vue2的beforeMount功能相同。
  • onMounted:和vue2的mounted功能相同。
  1. 更新阶段
  • onBeforeUpdate:和vue2的beforeUpdate功能相同。
  • onUpdated:和vue2的updated功能相同。
  1. 销毁阶段
  • onBeforeUnmount:和vue2的beforeDestroy功能相同。
  • onUnmounted:和vue2的destroyed功能相同。
  1. 其他钩子
  • onActivated:当组件被激活时调用,用于keep-alive包裹的组件。
  • onDeactivated:当组件被停用时调用,用于keep-alive包裹的组件。
  • onErrorCaptured:当捕获到后代组件的错误时调用。

代码示例

数据双向绑定代码示例

vue2的数据双向绑定
<template>
  <div>
    
    <input type="text" v-model="message">
    <p>{{ message }}p>
    
    
    <input type="checkbox" v-model="isChecked">
    <p>是否选中:{{ isChecked }}p>
    
    
    <select v-model="selected">
      <option value="1">选项1option>
      <option value="2">选项2option>
      <option value="3">选项3option>
    select>
    <p>选中的值:{{ selected }}p>
  div>
template>

<script>
export default {
  // vue2中通过data函数定义响应式数据
  data() {
    return {
      message: 'Hello Vue2',
      isChecked: false,
      selected: '1'
    }
  }
}
script>
vue3的数据双向绑定
<template>
  <div>
    
    <input type="text" v-model="message">
    <p>{{ message }}p>
    
    
    <input type="checkbox" v-model="isChecked">
    <p>是否选中:{{ isChecked }}p>
    
    
    <select v-model="selected">
      <option value="1">选项1option>
      <option value="2">选项2option>
      <option value="3">选项3option>
    select>
    <p>选中的值:{{ selected }}p>
  div>
template>

<script setup>
// 导入vue3的ref函数
import { ref } from 'vue'

// 用ref定义响应式数据,基本类型和对象都可以
const message = ref('Hello Vue3')
const isChecked = ref(false)
const selected = ref('1')
script>

组件传值代码示例

vue2的组件传值
  1. 父传子

<template>
  <div>
    <h2>父组件h2>
    
    <Child :parentMessage="message" :userInfo="user">Child>
  div>
template>

<script>
import Child from './Child.vue'

export default {
  components: {
    Child
  },
  data() {
    return {
      message: '我是父组件的数据',
      user: {
        name: '张三',
        age: 18
      }
    }
  }
}
script>


<template>
  <div>
    <h3>子组件h3>
    <p>父组件传递的字符串:{{ parentMessage }}p>
    <p>父组件传递的对象:{{ userInfo.name }} - {{ userInfo.age }}p>
  div>
template>

<script>
export default {
  // 通过props接收父组件传递的数据
  props: {
    // 字符串类型
    parentMessage: String,
    // 对象类型
    userInfo: {
      type: Object,
      required: true
    }
  }
}
script>
  1. 子传父

<template>
  <div>
    <h2>父组件h2>
    <p>子组件传递的数据:{{ childMessage }}p>
    
    <Child @childEvent="handleChildEvent">Child>
  div>
template>

<script>
import Child from './Child.vue'

export default {
  components: {
    Child
  },
  data() {
    return {
      childMessage: ''
    }
  },
  methods: {
    // 处理子组件传递的数据
    handleChildEvent(data) {
      this.childMessage = data
    }
  }
}
script>


<template>
  <div>
    <h3>子组件h3>
    <button @click="sendDataToParent">向父组件传递数据button>
  div>
template>

<script>
export default {
  methods: {
    sendDataToParent() {
      // 通过$emit触发自定义事件,传递数据
      this.$emit('childEvent', '我是子组件的数据')
    }
  }
}
script>
  1. 兄弟传值(EventBus)
// 新建 eventBus.js
import Vue from 'vue'
// 创建空的vue实例作为事件总线
export default new Vue()

<!-- 兄弟组件1 Brother1.vue -->
<template>
  <div>
    <h3>兄弟组件1</h3>
    <button @click="sendToBrother2">向兄弟组件2传递数据</button>
  </div>
</template>

<script>
import eventBus from './eventBus.js'

export default {
  methods: {
    sendToBrother2() {
      // 触发事件传递数据
      eventBus.$emit('brotherEvent', '我是兄弟组件1的数据')
    }
  }
}
</script>

<!-- 兄弟组件2 Brother2.vue -->
<template>
  <div>
    <h3>兄弟组件2</h3>
    <p>兄弟组件1传递的数据:{{ brotherMessage }}</p>
  </div>
</template>

<script>
import eventBus from './eventBus.js'

export default {
  data() {
    return {
      brotherMessage: ''
    }
  },
  mounted() {
    // 监听事件接收数据
    eventBus.$on('brotherEvent', (data) => {
      this.brotherMessage = data
    })
  },
  beforeDestroy() {
    // 销毁前移除事件监听,防止内存泄漏
    eventBus.$off('brotherEvent')
  }
}
</script>
  1. vuex传值
// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    count: 0,
    productList: []
  },
  mutations: {
    // 修改count
    increment(state) {
      state.count++
    },
    // 设置商品列表
    setProductList(state, list) {
      state.productList = list
    }
  },
  actions: {
    // 异步获取商品列表
    fetchProductList({ commit }) {
      // 模拟接口请求
      setTimeout(() => {
        const list = [
          { id: 1, name: '商品1' },
          { id: 2, name: '商品2' }
        ]
        commit('setProductList', list)
      }, 1000)
    }
  },
  getters: {
    // 计算商品总数
    productCount(state) {
      return state.productList.length
    }
  }
})

<!-- 组件 A.vue -->
<template>
  <div>
    <h3>组件A</h3>
    <p>count: {{ count }}</p>
    <button @click="addCount">增加count</button>
  </div>
</template>

<script>
import { mapState, mapMutations } from 'vuex'

export default {
  computed: {
    ...mapState(['count'])
  },
  methods: {
    ...mapMutations(['increment']),
    addCount() {
      this.increment()
    }
  }
}
</script>

<!-- 组件 B.vue -->
<template>
  <div>
    <h3>组件B</h3>
    <p>商品总数:{{ productCount }}</p>
    <button @click="getProducts">获取商品列表</button>
    <ul>
      <li v-for="item in productList" :key="item.id">{{ item.name }}</li>
    </ul>
  </div>
</template>

<script>
import { mapState, mapGetters, mapActions } from 'vuex'

export default {
  computed: {
    ...mapState(['productList']),
    ...mapGetters(['productCount'])
  },
  methods: {
    ...mapActions(['fetchProductList']),
    getProducts() {
      this.fetchProductList()
    }
  }
}
</script>
vue3的组件传值
  1. 父传子

<template>
  <div>
    <h2>父组件h2>
    <Child :parentMessage="message" :userInfo="user">Child>
  div>
template>

<script setup>
import { ref, reactive } from 'vue'
import Child from './Child.vue'

// 定义响应式数据
const message = ref('我是父组件的数据')
const user = reactive({
  name: '张三',
  age: 18
})
script>


<template>
  <div>
    <h3>子组件h3>
    <p>父组件传递的字符串:{{ parentMessage }}p>
    <p>父组件传递的对象:{{ userInfo.name }} - {{ userInfo.age }}p>
  div>
template>

<script setup>
// 在setup中通过defineProps接收父组件传递的数据
const props = defineProps({
  parentMessage: String,
  userInfo: {
    type: Object,
    required: true
  }
})
script>
  1. 子传父

<template>
  <div>
    <h2>父组件h2>
    <p>子组件传递的数据:{{ childMessage }}p>
    <Child @childEvent="handleChildEvent">Child>
  div>
template>

<script setup>
import { ref } from 'vue'
import Child from './Child.vue'

const childMessage = ref('')

// 处理子组件传递的数据
const handleChildEvent = (data) => {
  childMessage.value = data
}
script>


<template>
  <div>
    <h3>子组件h3>
    <button @click="sendDataToParent">向父组件传递数据button>
  div>
template>

<script setup>
// 定义可以触发的事件
const emit = defineEmits(['childEvent'])

const sendDataToParent = () => {
  // 触发事件传递数据
  emit('childEvent', '我是子组件的数据')
}
script>
  1. 兄弟传值(mitt)
// 安装 mitt:npm install mitt
// 新建 eventBus.js
import mitt from 'mitt'
// 创建事件总线实例
export const eventBus = mitt()

<!-- 兄弟组件1 Brother1.vue -->
<template>
  <div>
    <h3>兄弟组件1</h3>
    <button @click="sendToBrother2">向兄弟组件2传递数据</button>
  </div>
</template>

<script setup>
import { eventBus } from './eventBus.js'

const sendToBrother2 = () => {
  // 触发事件传递数据
  eventBus.emit('brotherEvent', '我是兄弟组件1的数据')
}
</script>

<!-- 兄弟组件2 Brother2.vue -->
<template>
  <div>
    <h3>兄弟组件2</h3>
    <p>兄弟组件1传递的数据:{{ brotherMessage }}</p>
  </div>
</template>

<script setup>
import { ref, onUnmounted } from 'vue'
import { eventBus } from './eventBus.js'

const brotherMessage = ref('')

// 监听事件接收数据
const handleBrotherEvent = (data) => {
  brotherMessage.value = data
}

eventBus.on('brotherEvent', handleBrotherEvent)

// 组件卸载时移除事件监听
onUnmounted(() => {
  eventBus.off('brotherEvent', handleBrotherEvent)
})
</script>
  1. pinia传值
// 安装 pinia:npm install pinia
// store/index.js
import { defineStore } from 'pinia'

// 定义store
export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
    productList: []
  }),
  actions: {
    // 增加count
    increment() {
      this.count++
    },
    // 异步获取商品列表
    async fetchProductList() {
      // 模拟接口请求
      await new Promise(resolve => setTimeout(resolve, 1000))
      this.productList = [
        { id: 1, name: '商品1' },
        { id: 2, name: '商品2' }
      ]
    }
  },
  getters: {
    // 计算商品总数
    productCount() {
      return this.productList.length
    }
  }
})

<!-- 组件 A.vue -->
<template>
  <div>
    <h3>组件A</h3>
    <p>count: {{ counterStore.count }}</p>
    <button @click="counterStore.increment">增加count</button>
  </div>
</template>

<script setup>
import { useCounterStore } from './store/index.js'

// 获取store实例
const counterStore = useCounterStore()
</script>

<!-- 组件 B.vue -->
<template>
  <div>
    <h3>组件B</h3>
    <p>商品总数:{{ counterStore.productCount }}</p>
    <button @click="counterStore.fetchProductList">获取商品列表</button>
    <ul>
      <li v-for="item in counterStore.productList" :key="item.id">{{ item.name }}</li>
    </ul>
  </div>
</template>

<script setup>
import { useCounterStore } from './store/index.js'

// 获取store实例
const counterStore = useCounterStore()
</script>

生命周期代码示例

vue2的生命周期
<template>
  <div>
    <h2>vue2生命周期示例h2>
    <p>{{ message }}p>
    <button @click="updateMessage">更新数据button>
  div>
template>

<script>
export default {
  data() {
    return {
      message: '初始数据'
    }
  },
  // 实例初始化后,数据观测和事件配置之前
  beforeCreate() {
    console.log('beforeCreate:', this.message) // undefined,数据未初始化
  },
  // 实例创建完成后
  created() {
    console.log('created:', this.message) // 初始数据,数据已初始化,DOM未生成
    // 可以在这里进行数据请求
    this.fetchData()
  },
  // 挂载开始之前
  beforeMount() {
    console.log('beforeMount:', document.querySelector('p')) // null,DOM未挂载
  },
  // 挂载完成后
  mounted() {
    console.log('mounted:', document.querySelector('p')) // 

初始数据

,DOM已挂载
// 可以在这里进行DOM操作 }, // 数据更新时 beforeUpdate() { console.log('beforeUpdate:', document.querySelector('p').textContent) // 初始数据,DOM未更新 }, // DOM更新完成后 updated() { console.log('updated:', document.querySelector('p').textContent) // 更新后的数据,DOM已更新 }, // 实例销毁之前 beforeDestroy() { console.log('beforeDestroy:实例即将销毁') // 可以在这里清除定时器、解绑事件等 }, // 实例销毁后 destroyed() { console.log('destroyed:实例已销毁') }, methods: { updateMessage() { this.message = '更新后的数据' }, fetchData() { // 模拟数据请求 setTimeout(() => { console.log('数据请求完成') }, 1000) } } }
script>
vue3的生命周期
<template>
  <div>
    <h2>vue3生命周期示例h2>
    <p>{{ message }}p>
    <button @click="updateMessage">更新数据button>
  div>
template>

<script setup>
import { ref, onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted } from 'vue'

// 定义响应式数据
const message = ref('初始数据')

// 相当于beforeCreate和created
console.log('setup:', message.value) // 初始数据

// 挂载开始之前
onBeforeMount(() => {
  console.log('onBeforeMount:', document.querySelector('p')) // null,DOM未挂载
})

// 挂载完成后
onMounted(() => {
  console.log('onMounted:', document.querySelector('p')) // 

初始数据

,DOM已挂载
// 可以在这里进行DOM操作和数据请求 fetchData() }) // 数据更新时 onBeforeUpdate(() => { console.log('onBeforeUpdate:', document.querySelector('p').textContent) // 初始数据,DOM未更新 }) // DOM更新完成后 onUpdated(() => { console.log('onUpdated:', document.querySelector('p').textContent) // 更新后的数据,DOM已更新 }) // 实例销毁之前 onBeforeUnmount(() => { console.log('onBeforeUnmount:实例即将销毁') // 可以在这里清除定时器、解绑事件等 }) // 实例销毁后 onUnmounted(() => { console.log('onUnmounted:实例已销毁') }) const updateMessage = () => { message.value = '更新后的数据' } const fetchData = () => { // 模拟数据请求 setTimeout(() => { console.log('数据请求完成') }, 1000) }
script>

对比效果

特性 vue2 vue3 差异点
双向绑定原理 Object.defineProperty Proxy vue3支持监听对象新增/删除属性、数组变化,效率更高
响应式数据定义 data函数 ref/reactive vue3需要手动导入ref和reactive,数据访问和修改方式不同
父传子 props选项 defineProps 用法类似,vue3在setup中使用defineProps
子传父 $emit方法 defineEmits vue3需要在defineEmits中声明事件,更规范
兄弟传值 EventBus mitt vue3移除了内置事件总线,推荐使用mitt库
全局状态管理 vuex pinia pinia简化了API,支持TypeScript,更符合vue3风格
生命周期钩子 beforeCreate、created等 setup、onBeforeMount等 vue3的setup替代了beforeCreate和created,部分钩子改名
组件写法 Options API Composition API(setup) vue3的Composition API更灵活,便于逻辑复用

面试题回答方法

面试题1:vue2和vue3的双向绑定原理有什么区别?

正常回答方法

vue2的双向绑定基于Object.defineProperty实现。它会遍历data中的属性,为每个属性添加getter和setter。当属性被访问时,getter会收集依赖;当属性被修改时,setter会通知依赖更新,从而实现数据驱动视图。同时,通过v-model指令,结合事件监听,实现视图驱动数据,完成双向绑定。但这种方式无法监听对象新增或删除的属性,也不能直接监听数组的变化,需要通过重写数组方法来实现。

vue3的双向绑定则使用Proxy实现。Proxy可以创建对象的代理,拦截对象的各种操作,包括属性的读取、修改、新增、删除等。当响应式数据发生变化时,Proxy会触发相应的拦截器,通知依赖更新。对于数组,Proxy也能直接监听其变化,无需重写方法。此外,vue3中通过ref和reactive函数来创建响应式数据,ref用于基本类型,reactive用于对象类型,使用起来更灵活。

两者的主要区别在于:vue3的双向绑定能监听更多类型的变化,性能更好,使用更灵活;而vue2在处理对象新增/删除属性和数组变化时存在局限性。

大白话回答方法

vue2的双向绑定就像给每个数据装了个摄像头,但这个摄像头有缺点:只能监控已经存在的数据,新增的数据监控不到,数组的变化也得特殊处理。当数据变化时,它会告诉视图更新。

vue3的双向绑定则像给整个对象装了个监控系统,不管是原来就有的数据,还是新增、删除的数据,甚至是数组的变化,都能监控到。而且这个监控系统效率更高,用起来也更方便。

简单说,vue3的双向绑定能力更强,能处理更多情况,这也是vue3性能更好的原因之一。

面试题2:vue2和vue3的组件传值方式有哪些异同?

正常回答方法

vue2和vue3的组件传值有一些相同点,比如父传子都可以通过props,跨级传值都可以用provide/inject。

不同点主要有:

  1. 父传子:vue2通过props选项接收,vue3在setup中用defineProps接收,写法略有不同。
  2. 子传父:vue2用$emit方法触发事件,vue3用defineEmits定义事件后再触发,vue3需要声明事件,更规范。
  3. 兄弟传值:vue2用EventBus(空vue实例),vue3推荐用mitt库,原理类似但实现方式不同。
  4. 全局状态管理:vue2用vuex,vue3推荐用pinia,pinia的API更简洁,支持TypeScript。

总体来说,vue3的组件传值方式在vue2的基础上进行了优化,更注重规范性和可维护性。

大白话回答方法

vue2和vue3组件传值,父传子和跨级传值的思路差不多,都是父组件给数据,子组件接收。

但子传父就不一样了,vue2里子组件直接喊一声($emit)就行,vue3里得先告诉大家要喊什么(defineEmits),再喊,更规矩一些。

兄弟组件传值,vue2用自己家的EventBus,vue3则推荐用外面的mitt,就像换了个更顺手的工具。

全局状态管理方面,vue2用vuex,步骤多一点;vue3用pinia,更简单,还支持TypeScript,用起来更舒服。

面试题3:vue2和vue3的生命周期有什么区别?

正常回答方法

vue2和vue3的生命周期在阶段划分上基本一致,都包括创建、挂载、更新、销毁阶段,但具体钩子函数有一些区别:

  1. 创建阶段:vue2有beforeCreate和created,vue3中这两个钩子被setup替代,setup在实例初始化时执行。
  2. 挂载阶段:vue2是beforeMount和mounted,vue3是onBeforeMount和onMounted,功能相同,名称略有变化。
  3. 更新阶段:vue2是beforeUpdate和updated,vue3是onBeforeUpdate和onUpdated,功能和名称对应。
  4. 销毁阶段:vue2是beforeDestroy和destroyed,vue3是onBeforeUnmount和onUnmounted,名称更直观,体现了“卸载”的概念。
  5. 其他钩子:vue3新增了onActivated和onDeactivated,用于keep-alive包裹的组件,vue2中对应的是activated和deactivated。

此外,vue3的生命周期钩子需要从vue中导入后使用,而vue2是作为选项直接定义在组件中。

大白话回答方法

vue2和vue3的生命周期就像人从出生到死亡的过程,阶段都差不多,但给每个阶段起的名字和做事的方式有点不一样。

vue2里有beforeCreate和created,vue3里合并成了setup,在组件刚初始化的时候就执行。

挂载、更新阶段的钩子,名字差不多,就是vue3的前面多了个“on”,功能一样。

销毁阶段的钩子,vue3改了名字,叫onBeforeUnmount和onUnmounted,更直白,就是说组件要被卸载了。

还有,vue3的生命周期钩子得先从vue里拿过来才能用,vue2直接就能用。

总结

vue2和vue3在双向绑定、组件传值和生命周期方面都有不少差异,但核心思想是一致的,都是为了更好地实现数据驱动视图,提高开发效率。

双向绑定上,vue3的Proxy相比vue2的Object.defineProperty,能监听更多类型的变化,性能更好,使用更灵活。这意味着在开发中,我们可以更自由地操作数据,不用担心数据变化无法被监听。

组件传值方面,vue3在vue2的基础上进行了优化,子传父需要声明事件,更规范;兄弟传值推荐用mitt;全局状态管理用pinia,这些变化让代码更易维护,团队协作更顺畅。

生命周期上,vue3的钩子函数名称更直观,setup函数替代了beforeCreate和created,使用起来更清晰。了解这些变化,能让我们在合适的时机执行相应的操作,比如在onMounted中进行DOM操作,在onBeforeUnmount中清理资源。

掌握这些差异,能帮助我们更好地在vue2和vue3项目中切换,写出高效、规范的代码。

扩展思考

扩展思考1:为什么vue3要改用Proxy实现双向绑定?

vue3改用Proxy实现双向绑定主要有以下几个原因:

  1. 更全面的监听能力:Object.defineProperty只能监听对象已存在的属性,对于新增或删除的属性无法监听,需要通过set和set和setdelete方法来处理。而Proxy可以拦截对象的所有操作,包括新增、删除属性,以及数组的变化,无需额外的方法。

  2. 更好的性能:Object.defineProperty需要遍历对象的每个属性,为每个属性添加getter和setter,当对象属性较多时,性能开销较大。而Proxy是对整个对象进行代理,不需要遍历属性,初始化性能更好。而且,Proxy的拦截操作是惰性的,只有当属性被访问或修改时才会触发,节省了不必要的开销。

  3. 更简洁的代码:使用Object.defineProperty实现响应式时,需要处理各种边界情况,比如数组的变化需要重写数组方法。而Proxy可以直接监听数组的变化,代码更简洁。

  4. 未来的扩展性:Proxy是ES6新增的特性,有更强大的功能和更好的扩展性。随着JavaScript的发展,Proxy可能会支持更多的拦截操作,为vue的双向绑定提供更多可能性。

扩展思考2:vue3的Composition API相比vue2的Options API有什么优势?

vue3的Composition API相比vue2的Options API有以下优势:

  1. 更好的逻辑复用:Options API中,逻辑复用主要通过mixins实现,但mixins存在命名冲突、逻辑来源不清晰等问题。而Composition API可以将相关的逻辑封装在一个函数中,通过组合函数的方式实现逻辑复用,代码更清晰,避免了命名冲突。

  2. 更灵活的代码组织:Options API中,代码需要按照data、methods、computed等选项来组织,当组件逻辑复杂时,相关的代码可能会分散在不同的选项中,不利于维护。而Composition API可以将相关的逻辑集中在一起,按照功能组织代码,更符合人的思维习惯。

  3. 更好的类型支持:Composition API是基于函数的,更符合TypeScript的类型系统,能提供更好的类型推断和类型检查,减少类型错误。

  4. 更适合大型项目:在大型项目中,组件逻辑往往比较复杂,Composition API的逻辑复用和代码组织优势更加明显,能提高开发效率和代码质量。

扩展思考3:pinia相比vuex有哪些改进?

pinia相比vuex有以下改进:

  1. 更简洁的API:pinia移除了vuex中的mutation,直接通过action修改状态,减少了代码量。同时,pinia的API更简洁,使用起来更方便。

  2. 更好的TypeScript支持:pinia是用TypeScript编写的,天生支持TypeScript,能提供更好的类型推断和类型检查,开发体验更好。

  3. 不需要嵌套模块:vuex中,为了避免命名冲突,往往需要使用嵌套模块,增加了代码的复杂度。而pinia中可以创建多个store实例,每个store实例独立存在,不需要嵌套,代码更清晰。

  4. 更灵活的状态管理:pinia中,状态可以是任意类型,包括对象、数组、基本类型等,使用起来更灵活。同时,pinia支持插件扩展,可以根据需要扩展其功能。

  5. 与vue3的Composition API更好地集成:pinia的设计理念与vue3的Composition API一致,能更好地结合使用,提高开发效率。

扩展思考4:vue3的setup函数有什么特点和注意事项?

vue3的setup函数是Composition API的入口,具有以下特点:

  1. 执行时机早:setup函数在组件实例初始化后、beforeCreate之前执行,此时组件实例还未创建,无法访问this。

  2. 返回值:setup函数的返回值可以是一个对象或一个渲染函数。如果返回一个对象,对象中的属性和方法可以在模板中直接使用;如果返回一个渲染函数,可以自定义组件的渲染逻辑。

  3. 参数:setup函数有两个参数,props和context。props是组件的属性,是响应式的,不能直接解构;context是一个对象,包含attrs、slots、emit等属性,用于访问组件的属性、插槽和触发事件。

使用setup函数时需要注意以下事项:

  1. 不能在setup函数中使用this,因为此时组件实例还未创建。

  2. props是响应式的,不能直接解构,否则会失去响应性。如果需要解构,可以使用toRefs函数。

  3. setup函数中创建的响应式数据,需要通过return返回后才能在模板中使用。

  4. setup函数中可以使用vue3的生命周期钩子,但需要从vue中导入后使用,且钩子函数的名称前面多了一个“on”。

结尾

vue2和vue3的差异看似很多,但只要理解了背后的原理,就能轻松掌握。双向绑定从Object.defineProperty到Proxy的升级,让数据监听更全面高效;组件传值方式的优化,让代码更规范易维护;生命周期钩子的调整,让命名更直观。

回想一下,你是不是也遇到过这些情况:

  • 用vue2时,因为没注意到Object.defineProperty的局限性,导致数据更新后视图没变化
  • 在vue3项目中,用vue2的组件传值写法,结果传值失败
  • 混淆了vue2和vue3的生命周期钩子,导致代码执行时机不对

这些问题的根源都是没搞懂两者的差异。现在再遇到类似问题,你应该能快速定位原因,用今天学到的方法解决。

最后送大家一句口诀:“vue2 vue3有差异,双向绑定是核心;Proxy替代defineProperty,监听能力更强劲;组件传值方式多,父子兄弟要分清;生命周期记清楚,执行时机别弄错;掌握差异勤实践,开发效率节节升。” 希望这句口诀能帮你记住今天的知识点!

你可能感兴趣的:(vue2和vue3 实现数据双向绑定的原理详解,vue2和vue3 组件传值详解,vue2和vue3的生命周周期中都进行了什么事情详解?)