Vue3相较于Vue2的区别通过例子简单理解

仅供学习,有错误请在评论区指出~ 

1.Vue3源码组织方式变化:使用 TS 重写

Vue2

Vue 2的源码主要使用ES6+编写,基于类的继承和原型链组织代码,在类型检查方面相对薄弱。Vue 2中的类型仅通过JSDoc注释或Flow进行简单标注,不支持完整的类型推断和检查。这导致在大型项目中,代码重构和维护较为困难,特别是涉及复杂数据结构时。

// Vue 2 源码结构示例(基于JavaScript + Flow)
export default class Vue {
  constructor(options) {
    this._init(options)
  }
  
  _init(options) {
    // 初始化逻辑
  }
  
  $mount(el) {
    // 挂载逻辑
  }
  
  // 其他方法...
}

Vue3

Vue 3完全使用TypeScript重写,采用了基于函数式编程的组织方式,提供了更严格的类型定义和检查。源码结构更加模块化,各个功能以独立包的形式发布,可以按需引入。TypeScript的静态类型系统使得代码更加健壮,能够在编译时捕获类型错误,提供了更好的IDE支持,如自动补全和类型提示。

// Vue 3 源码结构示例(基于TypeScript)
// 核心运行时
export interface App<HostElement = any> {
  mount(rootContainer: HostElement | string): ComponentPublicInstance
  unmount(): void
  // 其他属性和方法...
}

export const createApp = <HostElement>(
  rootComponent: Component,
  rootProps?: Data | null
): App<HostElement> => {
  // 创建应用逻辑
  return app
}

// 响应式系统
export function reactive<T extends object>(target: T): UnwrapNestedRefs<T> {
  // 实现响应式逻辑
}

// 组件实现
export function defineComponent<Props, RawBindings>(
  setup: (props: Readonly<Props>) => RawBindings | RenderFunction
): Component<Props>

解释

  • Vue 2 使用纯 JavaScript,类型检查需要额外工具。
  • Vue 3 使用 TypeScript 编写,支持类型推断和静态检查。

2.Vue3支持 Composition API,Vue2只能用Options API

Vue 2 实现 (Options API)

Vue 2主要使用Options API组织组件,将组件的不同功能部分分散到不同的选项中:data、methods、computed、watch等。这种方式使得同一个逻辑关注点的代码被分散到不同位置,在大型组件中难以维护。

export default {
  data() {
    return {
      count: 0
    };
  },
  methods: {
    increment() {
      this.count++;
    }
  }
};

Vue 3 实现 (Composition API)

Vue 3引入了Composition API,允许开发者按照逻辑关注点组织代码。相关的状态和方法可以组合在一起,形成可复用的逻辑单元。这种方式使得代码更加模块化,便于理解和维护,特别是在大型组件中。Composition API还支持更好的代码复用,通过自定义组合函数(composables)可以在多个组件间共享逻辑

import { ref } from 'vue';

export default {
  setup() {
    const count = ref(0);
    const increment = () => {
      count.value++;
    };
    return { count, increment };
  }
};

解释:

  • Vue 2 使用 Options API,逻辑分散在 datamethods 等选项中。
  • Vue 3 使用 Composition API,将相关逻辑集中在一个函数中,更灵活且易于复用。

3. 响应式系统提升

Vue 2 实现 (Object.defineProperty)

Vue 2使用Object.defineProperty实现响应式系统,通过劫持对象属性的getter和setter来追踪依赖和触发更新。这种实现方式有几个显著的限制:无法检测对象属性的添加或删除;无法直接检测数组索引的变化和长度的变化;需要对每个属性单独处理,性能开销较大。

export default {
  data() {
    return {
      state: {
        count: 0
      }
    };
  },
  mounted() {
    // 无法监听新增属性
    this.state.newProperty = 42; // 不会触发视图更新
  }
};

Vue 3 实现 (Proxy)

Vue 3使用ES6的Proxy实现响应式系统,可以拦截对象的多种操作,包括属性访问、属性设置、新增属性、删除属性等。这种实现方式克服了Vue 2的限制,可以监听到对象属性的动态添加和删除,以及数组的索引变化和长度变化,而且性能更好,因为Proxy是对整个对象进行代理,而不是对每个属性。

import { reactive } from 'vue';

export default {
  setup() {
    const state = reactive({ count: 0 });
    state.newProperty = 42; // 可以监听到新属性并触发视图更新
    return { state };
  }
};

解释:

  • Vue 2 使用 Object.defineProperty,无法监听动态添加或删除的属性。
  • Vue 3 使用 Proxy,可以监听对象的所有变化(包括新增和删除属性)。

4. 编译优化:静态节点标记与提升

Vue 2 实现

Vue 2的模板编译过程会识别静态根节点(包含至少一个静态子节点的节点),并在渲染函数中进行标记,但优化有限。在更新过程中,虚拟DOM的diff算法仍需要遍历整个组件树,包括静态内容,只是对标记为静态的子树跳过详细比对

// Vue 2编译后的渲染函数示例
export default {
  render(h) {
    return h('div', [
      h('h1', this.title),
      // 静态节点标记,但仍然会创建VNode
      h('p', { staticClass: 'static-text' }, '这是静态文本'),
      h('div', { staticClass: 'static-container' }, [
        h('p', '另一个静态文本')
      ]),
      h('p', this.dynamicMessage)
    ])
  }
}

// Vue 2的更新过程仍然需要遍历整个组件树
function updateComponent() {
  // 生成新的VNode树
  const vnode = vm._render()
  // 对比新旧VNode树,包括静态部分
  vm._update(vnode)
}

Vue 3 实现

Vue 3引入了更先进的编译优化策略,包括静态节点提升、补丁标记和块级树结构。静态节点提升将不变的内容在编译时提取出来,只创建一次;补丁标记对动态内容进行标记,只更新有变化的部分;块级树结构则将模板分割成带有动态内容的块,使得更新时只需要遍历动态内容,大大减少了比对的节点数量。

// Vue 3编译后的渲染函数示例
import { createVNode, createStaticVNode, openBlock, createBlock } from 'vue'

// 静态内容只创建一次,在整个组件生命周期内复用
const _hoisted_1 = /*#__PURE__*/createStaticVNode("<p class=\"static-text\">这是静态文本</p><div class=\"static-container\"><p>另一个静态文本</p></div>", 2)

export default {
  setup() {
    return { title, dynamicMessage }
  },
  render(_ctx, _cache) {
    return (openBlock(), createBlock("div", null, [
      // 动态内容使用PatchFlag标记具体需要更新的部分
      createVNode("h1", null, _ctx.title, 1 /* TEXT */),
      // 静态内容被提升,不参与更新过程
      _hoisted_1,
      // 动态内容使用PatchFlag标记
      createVNode("p", null, _ctx.dynamicMessage, 1 /* TEXT */)
    ]))
  }
}

// Vue 3的更新过程只需要处理动态内容
function updateComponent() {
  // 只对带有PatchFlag的节点进行更新
  // 静态内容(_hoisted_1)被跳过
}

解释:

  • Vue 2虽然能标记静态根节点,但每次更新仍会重新创建虚拟节点并遍历整个组件树;Vue 3引入了静态节点提升(只创建一次)、补丁标记(精确标记动态内容类型)和块级树结构(只遍历动态内容),使得更新性能显著提升,特别是在大型应用中。Vue 3的编译优化使得模板渲染性能接近手写渲染函数的性能,同时保持了模板的开发便利性。

5. 生命周期的变化:使用setup代替了之前的beforeCreate和created

Vue 2 实现

Vue 2中有八个主要的生命周期钩子,按调用顺序依次为:beforeCreate、created、beforeMount、mounted、beforeUpdate、updated、beforeDestroy、destroyed。这些钩子函数作为组件选项直接定义,在组件实例的特定阶段被调用。

// Vue 2生命周期钩子示例
export default {
  data() {
    return {
      message: 'Hello Vue 2'
    }
  },
  
  // 组件实例刚被创建,还没有初始化数据和事件
  beforeCreate() {
    console.log('beforeCreate hook')
    // 此时无法访问数据和方法
    console.log('message:', this.message) // undefined
  },
  
  // 组件实例已创建,数据已绑定,但DOM还未挂载
  created() {
    console.log('created hook')
    // 可以访问数据和方法
    console.log('message:', this.message) // "Hello Vue 2"
    // 可以进行数据请求、事件监听等操作
    this.fetchData()
  },
  
  // DOM挂载前调用
  beforeMount() {
    console.log('beforeMount hook')
  },
  
  // DOM挂载完成后调用
  mounted() {
    console.log('mounted hook')
    // 可以操作DOM
    console.log(this.$el)
  },
  
  // 数据更新时,虚拟DOM重新渲染前调用
  beforeUpdate() {
    console.log('beforeUpdate hook')
  },
  
  // 虚拟DOM重新渲染后调用
  updated() {
    console.log('updated hook')
  },
  
  // 组件销毁前调用
  beforeDestroy() {
    console.log('beforeDestroy hook')
    // 清理工作:解除事件监听、取消定时器等
    this.cleanup()
  },
  
  // 组件销毁后调用
  destroyed() {
    console.log('destroyed hook')
  },
  
  methods: {
    fetchData() {
      // 数据获取逻辑
    },
    cleanup() {
      // 清理逻辑
    }
  }
}

Vue 3 实现

Vue 3中,生命周期钩子通过从Vue导入的函数使用,在setup函数中注册。beforeCreate和created钩子被setup函数本身替代,其他钩子则有对应的函数:onBeforeMount、onMounted、onBeforeUpdate、onUpdated、onBeforeUnmount(替代beforeDestroy)、onUnmounted(替代destroyed)。此外,Vue 3还新增了组合式API特有的钩子,如onRenderTracked和onRenderTriggered。

// Vue 3生命周期钩子示例
import {
  ref,
  onBeforeMount,
  onMounted,
  onBeforeUpdate,
  onUpdated,
  onBeforeUnmount,
  onUnmounted,
  onRenderTracked,
  onRenderTriggered
} from 'vue'

export default {
  // setup函数在组件实例创建之初被调用,相当于beforeCreate和created的结合
  setup() {
    console.log('setup function - replaces beforeCreate & created')
    
    const message = ref('Hello Vue 3')
    
    // 在这里可以访问响应式数据和方法
    console.log('message:', message.value) // "Hello Vue 3"
    
    // 可以进行数据请求、事件监听等操作
    fetchData()
    
    // DOM挂载前调用
    onBeforeMount(() => {
      console.log('onBeforeMount hook')
    })
    
    // DOM挂载完成后调用
    onMounted(() => {
      console.log('onMounted hook')
      // 可以操作DOM
      console.log(document.querySelector('#app'))
    })
    
    // 数据更新时,虚拟DOM重新渲染前调用
    onBeforeUpdate(() => {
      console.log('onBeforeUpdate hook')
    })
    
    // 虚拟DOM重新渲染后调用
    onUpdated(() => {
      console.log('onUpdated hook')
    })
    
    // 组件卸载前调用(替代beforeDestroy)
    onBeforeUnmount(() => {
      console.log('onBeforeUnmount hook')
      // 清理工作:解除事件监听、取消定时器等
      cleanup()
    })
    
    // 组件卸载后调用(替代destroyed)
    onUnmounted(() => {
      console.log('onUnmounted hook')
    })
    
    // Vue 3新增:当组件的渲染依赖项被追踪时调用
    onRenderTracked((event) => {
      console.log('onRenderTracked', event)
    })
    
    // Vue 3新增:当组件的渲染依赖项触发重新渲染时调用
    onRenderTriggered((event) => {
      console.log('onRenderTriggered', event)
    })
    
    function fetchData() {
      // 数据获取逻辑
    }
    
    function cleanup() {
      // 清理逻辑
    }
    
    // 必须返回要暴露给模板的内容
    return {
      message,
      fetchData
    }
  }
}

解释:Vue 2使用选项式API定义生命周期钩子,有beforeCreate、created等八个主要钩子;Vue 3通过组合式API中的函数注册生命周期钩子,setup函数替代了beforeCreate和created,其他钩子改为onBeforeMount、onMounted等形式,销毁相关钩子改名为Unmount,并新增了渲染追踪相关钩子。这种变化使得生命周期钩子可以在逻辑关注点中组织,而不是分散在不同选项中,提高了代码的可组织性和复用性。

6.打包体积优化:移除了一些不常用的API

Vue 2 实现

Vue 2将所有功能打包在一起,即使不使用某些功能,它们也会被包含在最终的构建中。Vue 2支持的功能如过滤器(filters)、内联模板(inline-template)等在单一包中提供,无法通过摇树优化移除未使用的功能。

// Vue 2中的过滤器使用示例
export default {
  data() {
    return {
      price: 100
    }
  },
  // 定义过滤器
  filters: {
    currency(value) {
      return '$' + value.toFixed(2)
    }
  },
  // 使用内联模板
  template: `
    <div>
      <p>价格: {{ price | currency }}</p>
      <child-component inline-template>
        <div>
          <!-- 这里可以访问子组件的数据 -->
          <p>我是子组件的内容</p>
        </div>
      </child-component>
    </div>
  `
}

Vue 3 实现

Vue 3采用了基于ES模块的模块化架构,支持摇树优化(tree-shaking),未使用的功能可以在构建时被移除,减小最终的包大小。同时,Vue 3移除了一些不常用或有更好替代方案的功能,如过滤器(推荐使用计算属性或方法)和内联模板(推荐使用作用域插槽)。

// Vue 3中不再支持过滤器,改用计算属性或方法
import { ref, computed } from 'vue'

export default {
  setup() {
    const price = ref(100)
    
    // 使用计算属性替代过滤器
    const formattedPrice = computed(() => {
      return '$' + price.value.toFixed(2)
    })
    
    // 或使用方法
    function formatCurrency(value) {
      return '$' + value.toFixed(2)
    }
    
    return {
      price,
      formattedPrice,
      formatCurrency
    }
  },
  // 使用作用域插槽替代内联模板
  template: `
    <div>
      <p>价格: {{ formattedPrice }}</p>
      <child-component>
        <template #default="slotProps">
          <!-- 通过插槽props访问子组件数据 -->
          <div>
            <p>我是子组件的内容</p>
          </div>
        </template>
      </child-component>
    </div>
  `
}

Vue 2将所有功能打包在一起,支持filters、inline-template等特性,但包体积较大且无法优化;Vue 3采用模块化设计和tree-shaking技术,移除了不常用API(如filters、inline-template),并提供了更好的替代方案,使得最终打包体积更小,加载性能更好。Vue 3的全局API也改为了显式导入的方式,进一步支持了tree-shaking优化。

7. Vuex状态管理

Vue 2 实现

Vue 2中使用Vuex需要先通过Vue.use(Vuex)注册插件,然后使用new Vuex.Store()创建store实例。在组件中,通过this.$store访问store实例,使用mapState、mapGetters等辅助函数将store中的状态映射到组件中。

// store.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    count: 0,
    todos: []
  },
  mutations: {
    increment(state) {
      state.count++
    },
    addTodo(state, todo) {
      state.todos.push(todo)
    }
  },
  actions: {
    incrementAsync({ commit }) {
      setTimeout(() => {
        commit('increment')
      }, 1000)
    },
    fetchTodos({ commit }) {
      return fetch('/api/todos')
        .then(response => response.json())
        .then(todos => {
          todos.forEach(todo => commit('addTodo', todo))
        })
    }
  },
  getters: {
    completedTodos: state => {
      return state.todos.filter(todo => todo.completed)
    },
    todoCount: state => {
      return state.todos.length
    }
  }
})

// main.js
import Vue from 'vue'
import App from './App.vue'
import store from './store'

new Vue({
  store, // 将store注入到Vue实例
  render: h => h(App)
}).$mount('#app')

// 组件中使用
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex'

export default {
  computed: {
    // 直接访问store
    localCount() {
      return this.$store.state.count
    },
    // 使用mapState辅助函数
    ...mapState({
      count: state => state.count,
      todos: 'todos' // 字符串形式,等同于 state => state.todos
    }),
    // 使用mapGetters辅助函数
    ...mapGetters(['completedTodos', 'todoCount'])
  },
  methods: {
    // 直接访问store
    incrementDirect() {
      this.$store.commit('increment')
    },
    fetchTodosDirect() {
      this.$store.dispatch('fetchTodos')
    },
    // 使用mapMutations辅助函数
    ...mapMutations(['increment', 'addTodo']),
    // 使用mapActions辅助函数
    ...mapActions(['incrementAsync', 'fetchTodos'])
  }
}

Vue 3 实现

Vue 3中,Vuex 4利用Vue 3的新特性,通过createStore函数创建store实例,并通过app.use(store)将其安装到应用中。在组件中,使用useStore()组合式函数获取store实例,结合组合式API使用。

// store.js
import { createStore } from 'vuex'

export const store = createStore({
  state() {
    return {
      count: 0,
      todos: []
    }
  },
  mutations: {
    increment(state) {
      state.count++
    },
    addTodo(state, todo) {
      state.todos.push(todo)
    }
  },
  actions: {
    incrementAsync({ commit }) {
      setTimeout(() => {
        commit('increment')
      }, 1000)
    },
    fetchTodos({ commit }) {
      return fetch('/api/todos')
        .then(response => response.json())
        .then(todos => {
          todos.forEach(todo => commit('addTodo', todo))
        })
    }
  },
  getters: {
    completedTodos: state => {
      return state.todos.filter(todo => todo.completed)
    },
    todoCount: state => {
      return state.todos.length
    }
  }
})

// main.js
import { createApp } from 'vue'
import App from './App.vue'
import { store } from './store'

const app = createApp(App)
app.use(store) // 新的API风格
app.mount('#app')

// 组件中使用 - 选项式API
export default {
  computed: {
    // 可以继续使用mapState等辅助函数
    ...mapState(['count', 'todos']),
    ...mapGetters(['completedTodos', 'todoCount'])
  },
  methods: {
    ...mapMutations(['increment']),
    ...mapActions(['fetchTodos'])
  }
}

// 组件中使用 - 组合式API
import { useStore } from 'vuex'
import { computed } from 'vue'

export default {
  setup() {
    const store = useStore()
    
    // 访问state
    const count = computed(() => store.state.count)
    const todos = computed(() => store.state.todos)
    
    // 访问getters
    const completedTodos = computed(() => store.getters.completedTodos)
    const todoCount = computed(() => store.getters.todoCount)
    
    // 使用mutations
    function increment() {
      store.commit('increment')
    }
    
    // 使用actions
    function fetchTodos() {
      store.dispatch('fetchTodos')
    }
    
    return {
      count,
      todos,
      completedTodos,
      todoCount,
      increment,
      fetchTodos
    }
  }
}

解释:Vue 2使用new Vuex.Store()创建store实例,通过this.$store访问,偏向选项式API;Vue 3使用createStore()创建实例,通过useStore()函数在组合式API中获取store,这与Vue 3整体的API风格保持一致,更适合函数式编程和组合逻辑。虽然API形式有变化,但Vuex的核心概念(state、getters、mutations、actions)在Vue 3中保持不变,确保了平滑升级。

8. Route 获取页面实例与路由信息:组合式API的访问方式

Vue 2 实现

在Vue 2中,路由实例通过this.router访问,当前路由信息通过this.router访问,当前路由信息通过this. router访问,当前路由信息通过this.route访问。这与Vue 2的选项式API保持一致,依赖于组件实例上下文。

// Vue 2中访问路由
export default {
  created() {
    // 访问当前路由信息
    console.log('Current path:', this.$route.path)
    console.log('Route params:', this.$route.params)
    console.log('Query string:', this.$route.query)
    
    // 路由导航
    this.goToHome()
  },
  methods: {
    goToHome() {
      this.$router.push('/home')
    },
    goBack() {
      this.$router.go(-1)
    },
    navigateWithParams() {
      this.$router.push({
        name: 'user',
        params: { id: 123 },
        query: { plan: 'premium' }
      })
    },
    navigateWithReplace() {
      // 替换当前历史记录,而不是添加新记录
      this.$router.replace('/login')
    }
  },
  // 利用路由信息计算属性
  computed: {
    isHomePage() {
      return this.$route.path === '/home'
    },
    userName() {
      return this.$route.params.username
    },
    userQueryParams() {
      return this.$route.query
    }
  },
  // 监听路由变化
  watch: {
    '$route'(to, from) {
      console.log('Route changed from', from.path, 'to', to.path)
      // 执行页面特定逻辑
      this.loadPageData()
    }
  }
}

Vue 3 实现

Vue 3中,为了适应组合式API,Vue Router 4提供了useRouter和useRoute组合函数来获取路由实例和当前路由信息。这些函数只能在setup函数或组合式函数中使用,符合Vue 3的设计理念。

// Vue 3中访问路由
import { computed, watch } from 'vue'
import { useRouter, useRoute } from 'vue-router'

export default {
  setup() {
    // 获取路由实例和当前路由
    const router = useRouter()
    const route = useRoute()
    
    // 访问当前路由信息
    console.log('Current path:', route.path)
    console.log('Route params:', route.params)
    console.log('Query string:', route.query)
    
    // 路由导航方法
    function goToHome() {
      router.push('/home')
    }
    
    function goBack() {
      router.go(-1)
    }
    
    function navigateWithParams() {
      router.push({
        name: 'user',
        params: { id: 123 },
        query: { plan: 'premium' }
      })
    }
    
    function navigateWithReplace() {
      router.replace('/login')
    }
    
    // 利用路由信息的计算属性
    const isHomePage = computed(() => route.path === '/home')
    const userName = computed(() => route.params.username)
    const userQueryParams = computed(() => route.query)
    
    // 监听路由变化
    watch(
      () => route.path,
      (newPath, oldPath) => {
        console.log('Route changed from', oldPath, 'to', newPath)
        // 执行页面特定逻辑
        loadPageData()
      }
    )
    
    function loadPageData() {
      // 加载页面数据
    }
    
    // 需要返回以供模板使用
    return {
      route,  // 可以在模板中使用route.path等
      goToHome,
      goBack,
      navigateWithParams,
      navigateWithReplace,
      isHomePage,
      userName,
      userQueryParams
    }
  }
}

// 还可以提取为可复用的组合函数
function useNavigation() {
  const router = useRouter()
  
  function goToHome() {
    router.push('/home')
  }
  
  function goToUser(id) {
    router.push({ name: 'user', params: { id } })
  }
  
  return {
    goToHome,
    goToUser
  }
}

Vue 2通过this.router和this.router和this. router和this.route实例属性访问路由,符合选项式API风格;Vue 3引入useRouter和useRoute组合函数获取路由,符合组合式API风格,便于逻辑复用和提取。两者在功能上基本等同,但API设计理念不同,Vue 3的方式更加灵活,可以结合其他组合式函数创建复杂的导航逻辑。Vue 3也保留了对选项式API的支持,仍可使用this.router和this.router和this. router和this.route,确保平滑迁移。

9. Props 的使用变化:访问方式不同

Vue2实现

Vue 2中,props定义在props选项中,通过this访问props数据,这与访问data和computed等其他选项中定义的数据方式保持一致。

// Vue 2中的props
export default {
  props: {
    // 简单类型检查
    title: String,
    // 详细类型定义 
    user: {
      type: Object,
      required: true
    },
    // 带默认值
    isAdmin: {
      type: Boolean,
      default: false
    },
    // 自定义验证
    level: {
      type: Number,
      validator: function(value) {
        return value >= 0 && value <= 100
      }
    }
  },
  
  data() {
    return {
      localTitle: this.title // 基于props初始化本地状态
    }
  },
  
  computed: {
    formattedTitle() {
      return this.title.toUpperCase() // 通过this访问props
    },
    userName() {
      return this.user ? this.user.name : 'Anonymous' // 通过this访问props
    }
  },
  
  methods: {
    logProps() {
      console.log('Title:', this.title) // 通过this访问props
      console.log('User:', this.user)
      console.log('Is Admin:', this.isAdmin)
    },
    
    handleClick() {
      if (this.isAdmin) {
        // 管理员逻辑
      } else {
        // 普通用户逻辑
      }
    }
  },
  
  watch: {
    // 监听props变化
    title(newValue, oldValue) {
      console.log('Title changed from:', oldValue, 'to:', newValue)
      this.localTitle = newValue // 更新本地状态
    }
  }
}

Vue 3 实现

Vue 3中,在使用组合式API时,props作为setup函数的第一个参数传入,直接通过props对象访问,不再通过this。这与Vue 3整体的设计理念一致,避免使用this指向组件实例。

// Vue 3中的props
import { ref, computed, watch, toRefs } from 'vue'

export default {
  props: {
    // props定义方式与Vue 2相同
    title: String,
    user: {
      type: Object,
      required: true
    },
    isAdmin: {
      type: Boolean,
      default: false
    },
    level: {
      type: Number,
      validator: value => value >= 0 && value <= 100
    }
  },
  
  setup(props) {
    // 直接通过props参数访问props
    console.log('Title:', props.title)
    console.log('User:', props.user)
    
    // 创建本地响应式状态
    const localTitle = ref(props.title)
    
    // 使用计算属性处理props
    const formattedTitle = computed(() => props.title.toUpperCase())
    const userName = computed(() => props.user ? props.user.name : 'Anonymous')
    
    // 方法中访问props
    function logProps() {
      console.log('Title:', props.title)
      console.log('User:', props.user)
      console.log('Is Admin:', props.isAdmin)
    }
    
    function handleClick() {
      if (props.isAdmin) {
        // 管理员逻辑
      } else {
        // 普通用户逻辑
      }
    }
    
    // 监听props变化
    watch(
      () => props.title,
      (newValue, oldValue) => {
        console.log('Title changed from:', oldValue, 'to:', newValue)
        localTitle.value = newValue // 更新本地状态
      }
    )
    
    // 使用toRefs解构props,保持响应性
    const { isAdmin, level } = toRefs(props)
    
    // isAdmin是一个ref,需要通过.value访问
    const adminMessage = computed(() => {
      return isAdmin.value ? '你是管理员' : '你是普通用户'
    })
    
    return {
      localTitle,
      formattedTitle,
      userName,
      logProps,
      handleClick,
      adminMessage
    }
  }
}

Vue 2通过this关键字访问props,与访问data、computed等选项的方式一致,适合Options API;Vue 3在setup函数中通过props参数直接访问props,不依赖this,更加明确和直观,适合Composition API。此外,Vue 3提供了toRefs工具函数,可以将props对象的各个属性转为单独的ref,便于解构使用,同时保持响应性。尽管访问方式有所不同,但props的定义方式(类型声明、默认值、验证等)在Vue 3中基本保持不变。

10. 父子组件传值与emits定义

Vue2实现

Vue 2中,子组件通过$emit方法触发事件,向父组件传递数据,但没有显式声明可触发的事件列表。

// Vue 2 子组件
export default {
  data() {
    return {
      searchText: '',
      selectedItem: null
    }
  },
  methods: {
    // 触发事件并传递数据
    submitSearch() {
      this.$emit('search', this.searchText)
    },
    
    selectItem(item) {
      this.selectedItem = item
      this.$emit('select', item)
    },
    
    // 自定义事件名
    sendCustomData() {
      this.$emit('customEvent', { id: 123, name: 'Custom Data' })
    },
    
    // 多参数事件
    updateValues(value1, value2) {
      this.$emit('update', value1, value2, 'extra info')
    }
  }
}

// Vue 2 父组件
<template>
  <div>
    <child-component
      @search="handleSearch"
      @select="handleSelect"
      @customEvent="handleCustom"
      @update="handleUpdate"
    ></child-component>
  </div>
</template>

<script>
export default {
  methods: {
    handleSearch(text) {
      console.log('Search for:', text)
    },
    
    handleSelect(item) {
      console.log('Selected item:', item)
    },
    
    handleCustom(data) {
      console.log('Custom event data:', data)
    },
    
    handleUpdate(val1, val2, extraInfo) {
      console.log('Update values:', val1, val2, extraInfo)
    }
  }
}
</script>

Vue 3 实现

Vue 3引入了emits选项,用于显式声明组件可以触发的自定义事件。这有助于文档和类型推断,并且可以为事件添加验证。在setup函数中,通过context.emit触发事件(通常通过解构获取emit函数)。

// Vue 3 子组件
export default {
  // 显式定义可触发的事件
  emits: [
    'search',
    'select',
    'customEvent',
    'update',
    'backData' // 自定义名称的事件需要在这里声明
  ],
  
  // 带验证的emits
  // emits: {
  //   search: (text) => {
  //     // 返回true表示验证通过,false表示验证失败
  //     return typeof text === 'string' && text.length > 0
  //   },
  //   select: (item) => {
  //     return item !== null && typeof item === 'object'
  //   }
  // },
  
  setup(props, { emit }) {
    const searchText = ref('')
    const selectedItem = ref(null)
    
    // 触发事件并传递数据
    function submitSearch() {
      emit('search', searchText.value)
    }
    
    function selectItem(item) {
      selectedItem.value = item
      emit('select', item)
    }
    
    // 自定义事件名
    function sendCustomData() {
      emit('customEvent', { id: 123, name: 'Custom Data' })
    }
    
    // 自定义名称的事件,需在emits中定义
    function sendBackData() {
      emit('backData', { result: 'success', timestamp: Date.now() })
    }
    
    // 多参数事件
    function updateValues(value1, value2) {
      emit('update', value1, value2, 'extra info')
    }
    
    return {
      searchText,
      selectedItem,
      submitSearch,
      selectItem,
      sendCustomData,
      sendBackData,
      updateValues
    }
  }
}

// Vue 3 父组件
<template>
  <div>
    <child-component
      @search="handleSearch"
      @select="handleSelect"
      @customEvent="handleCustom"
      @backData="handleBackData"
      @update="handleUpdate"
    ></child-component>
  </div>
</template>

<script>
import { defineComponent } from 'vue'

export default defineComponent({
  setup() {
    function handleSearch(text) {
      console.log('Search for:', text)
    }
    
    function handleSelect(item) {
      console.log('Selected item:', item)
    }
    
    function handleCustom(data) {
      console.log('Custom event data:', data)
    }
    
    function handleBackData(data) {
      console.log('Back data:', data)
    }
    
    function handleUpdate(val1, val2, extraInfo) {
      console.log('Update values:', val1, val2, extraInfo)
    }
    
    return {
      handleSearch,
      handleSelect,
      handleCustom,
      handleBackData,
      handleUpdate
    }
  }
})
</script>

11.Vue3 的 template 模板支持多个根标签

Vue 2 实现

Vue 2的模板必须有且只有一个根节点,导致开发者常常需要添加不必要的包裹div。这一限制源于Vue的虚拟DOM实现,每个组件实例必须对应一个单一的DOM根元素。

<!-- Vue 2: 必须有一个根节点 -->
<template>
  <!-- 不能直接并列放置多个元素 -->
  <div class="wrapper"> <!-- 额外的包裹元素 -->
    <header>头部内容</header>
    <main>主要内容</main>
    <footer>底部内容</footer>
  </div>
</template>

<!-- 错误写法,Vue 2不支持 -->
<template>
  <header>头部内容</header>
  <main>主要内容</main>
  <footer>底部内容</footer>
</template>

Vue 3 实现

Vue 3引入了片段(Fragment)的概念,允许组件拥有多个根节点,无需额外的包裹元素。这一改进使得代码更加简洁,DOM结构更加扁平化,有利于样式控制和性能优化。

<!-- Vue 3: 支持多个根节点 -->
<template>
  <header>头部内容</header>
  <main>主要内容</main>
  <footer>底部内容</footer>
</template>

解释:
Vue 2要求模板必须有一个单一根节点,导致需要添加额外的包裹元素,增加了不必要的DOM嵌套;Vue 3引入Fragment概念,支持多个根节点并行排列,无需额外包裹元素,使DOM结构更扁平,有利于布局实现(特别是CSS Grid和Flexbox布局)和性能优化。这一变化不仅简化了代码,还提高了渲染性能和DOM操作效率。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值