安装


  1. 通过脚手架Vite【vue的轻量级前端开发与构建工具】
  npm init vite hello-vue3 -- --template vue
  1. 通过vue-cli脚手架
  vue create hello-vue3
  1. 通过CDN引入
  <script src="https://unpkg.com/vue@next"></script>

新特性


  • 组合式API
  • Teleport
  • 片段
  • 触发组件选项
  • 来自@vue/runtime-corecreateRendererAPI,用于创建定义渲染器
  • 单文件组合式API语法糖(<script setup>)
  • 单文件组件状态驱动的CSS变量(<style>中的v-bind)
  • SFC <style scoped> 现在可以包含全局规划或只针对插槽内容的规则
  • Suspense(实验阶段)

vue2到vue3中的变化


1. vue实例创建

在vue3中,没有组件构造器的概念

  //vue 2
  const app = new Vue({})
  //vue 3
  const app = createApp(App)
2. vue-router的使用

需要配置vue-router4.0以上版本

  //必须在app.mount('#app');之前  
  app.use(router);  // 使用的是router的实例,而不是构造器。
  //router.js
  import { createRouter, createWebHashHistory } from "vue-router";

  import Ref from './components/Ref.vue';
  const routes = [{
      path: '/ref',
      component: Ref,
  }];
  const router = createRouter({
      history: createWebHashHistory(),
      routes,
  })

  export default router;
3. vuex的使用

需要配置vuex4.0以上版本

  // 必须在app.mount('#app');之前
  app.use(store);
  import { createStore } from 'vuex'
  const store = createStore({
      state: {
          message: "this is the message from vuex store",
      }
  })
  export default store;
4. v-for中的ref数组

在vue2中,使用的refattribute会用ref数组填充对应的$refproperty。如果存在v-for嵌套的情况下,行为不明确且效率低

在vue3中不再自动创建$ref数组,需要从单个绑定获取多个ref,即将ref绑定到一个灵活的函数上

<ul>
    <li v-for='movie in movies' 
    :key='movie.id' 
    :ref="setMovieVNs">
    {{ movie.name }}</li>
</ul>
export default {
  name: "Ref",
  data() {
    return {
        movies:[{
            id:1,
            name:"长津湖"
        },{
            id:2,
            name:"铁道游击队"
        },{
            id:3,
            name:"金刚川"
        },],
        movieNVs:[],
    }
  },
  methods:{
      setMovieVNs(el){  //需要手动将创建的虚拟节点,放入自创数组中。
          if(el){
              this.movieNVs.push(el);
          }
      },
      showRef(){
          console.log(this.movieNVs);
      }
  }
}
5. 异步组件

概述:

  • 新的defineAsyncComponent,用于显式地定义异步组件
  • component选项被重命名为loader
  • Loader函数本身不再接收resolvereject参数,且必须返回一个Promise

什么是异步组件?

就是当有需要的时候,才会去进行加载,并且把结果缓存起来,供未来重载使用。

一般配合路由使用(Vue Router 2.4.0+)。但路由的懒加载,不能使用defineAsyncComponent

vue2

  const AsyncHomeComponent = () => import('./src/App.vue'); //简单写法
  // 高阶写法
  const AsyncHomeComponent = () => { 
    // 需要加载的组件
    component: import ('./src/App.vue'),
    // 正在加载的时候响应的组件
    loading: LoadingComponent,
    // 加载失败的时候响应的组件
    error: ErrorComponent,
    // 显示加载时组件的延时时间。(默认200ms)
    delay: 200,
    // 超时时间,超时则显示error组件。(默认`Infinity`)
    timeout: 3000,
}

vue3

import { defineAsyncComponent } from 'vue';
const AsyncHomeComponent = defineAsyncComponent({
    // 需要加载的组件
    laoder: () =>
        import ('./src/App.vue'),
    // 正在加载的时候响应的组件
    loading: LoadingComponent,
    // 加载失败的时候响应的组件
    error: ErrorComponent,
    // 显示加载时组件的延时时间。(默认200ms)
    delay: 200,
    // 超时时间,超时则显示error组件。(默认`Infinity`)
    timeout: 3000,
})
6. attribute强制行为

不影响绝大多数开发人员
attr='false’并不会移除,需要attr=null 或 attr=undefined

7. $attrs包含了class和style

$attrs包含了所有attribute

vue2:原本classstyle会被添加到根元素中。因为$attr不包括class和style。需要特殊处理

App.vue

  <template>
    <div id="app">
      <router-link to="/ref">ref测试</router-link>
      <router-view></router-view>

      <attr-test id="myAttr" class="myAttr" style="color:red" />
    </div>
  </template>

  <script>
    import AttrTest from './components/AttrTest.vue'
    export default {
      components: { AttrTest },
    }
  </script>

AttrTest.vue

  <template>
    <div>
        <p>AttrTest</p>
      <input type="text" v-bind="$attrs">
    </div>
  </template>

  <script type="text/javascript">
    export default {
      name: "AttrTest",
       inheritAttrs: false
    }
  </script>

vue2渲染结果:

  <div class="myAttr" style="color: red;">
    <p>AttrTest</p>
    <input type="text" id="myAttr">
  </div>

vue3渲染结果:

  <div>
    <p>AttrTest</p>
    <input type="text" id="myAttr" class="myAttr" style="color: red;" >
  </div>
8. $children移除

建议使用$refs访问子组件

9. 自定义指令

进行了钩子命名的修改或者添加新的钩子

  <p v-heighlight="'yellow'">高亮显示此文本</p>

在vue2中,有如下钩子:

  • bind:指令绑定到元素后执行一次
  • inserted:元素插入父DOM后执行一次
  • update:元素更新,子元素未更新时,调用一次
  • componentUpdated:组件和子元素都被更新,调用一次
  • unbind:指令移除,调用一次
  Vue.directive('highlight',{
    bind(el,binding,vnode){
      el.style.background = binding.value;
      const vm = vnode.context; //vue2中获取实例的方法
    }
  })

在vue3中,对钩子做了一定的修改:

  • created:元素的attribute或事件监听器被应用之前调用
  • beforeMount:绑定到元素后执行一次【原来的bind】
  • mounted:元素插入父DOM后执行一次【原来的inserted】
  • beforeUpdate:元素本身被更新之前,调用一次
  • updated:元素更新,子元素未更新,调用一次
  • beforeUnmount:指令被移除前,调用一次
  • unmounted:指令移除,调用一次。【原来的unbind】
  Vue.directive('highlight',{
    beforeMount(el,binding,vnode){
      el.style.background = binding.value;
      const vm = binding.instance; //vue3中获取实例的方法
    }
  })
10. 自主定义元素

使用如Web Components API定义的自主定义元素<new-button></new-button>,如何将其变为vue的自定义元素

vue2:

   Vue.config.ignoredElements = ['new-button']; //声明外部自定义元素

vue3:

  const app = createApp({});
  app.config.compilerOptions.isCustomElement = tag => tag === 'new-button'
11. 定制内置元素【is的表达不同】

对于<button is='new-button'>而言

vue2渲染后:

  <new-button></new-button>

vue3:

  document.createElement('button',{is:'new-button'}); //通过这个来渲染原生的button
12.特殊DOM内模板解析问题

在ul、ol、table、select中,他们的子元素时有所限制的,只能是li、tr、option

在vue2中,使用is来绕过限制

<table>
  <tr is="blog-post-row"></tr>
</table>

在vue3中,由于is的行为变化,将元素解析为vue组件需要添加vue:前缀

<table>
  <tr is="vue:blog-post-row"></tr>
</table> 
13.DATA选项

vue3中,只接受function定义、返回objectdata选项【换言之,不管子组件还是父组件,都需要用函数的形式定义选项data】

14.Mixin合并行为

个人感觉,不能使用Mixin,可读性太差了.

vue3中的Mixin以组件内数据为主,比如组件中说user对象具有id属性,而混合的对象中既有id也有name

对于vue2而言,会将二者融合,冲突数据以组件为主

对于vue3而言,不会融合,组件内有什么属性,就是什么.

15.新增emits选项

props类似,emits可以用于声明组件可以触发那些事件,同时也具有验证器.

强烈推荐使用emits,因为移除了.native修饰符.任何未在emits中声明的事件监听器都会被算入到$attrs中,默认绑定在根节点上.

16.移除事件API
  • $on
  • $off
  • $once
    被移除
17.移除过滤器filters

建议采用computed 或 methods替代

18.新增片段

支持多根节点(片段)

19.函数创建组件

vue2

  export default{
    functional:true,
    props:['level'],
    render(h,{props,data,children}){
      return h(`h${prorps.level}`,data,children);
    }
  }

<template functional>
  <component
    :is="`h${props.level}`"
    v-bind="attrs"
    v-on="listeners"
  />
</template>

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

vue3:

  • 函数创建,h变成了全局变量,attrs\slots位于context中.
  import {h} from 'vue';
  const Heading = (props,context)=>{
    return h(`h${props.level}`, context.attrs, context.slots);
  }
  Heading.props = ['level'];
  export default Heading;
  • 单文件,不需要写functional;$attrs囊括了几乎所有attribute.包括listeners
<template>
  <component
    v-bind:is="`h${$props.level}`"
    v-bind="$attrs"
  />
</template>

<script>
export default {
  props: ['level']
}
</script>
20.全局API
  • Vue.config

app.config替代

  • Vue.config.productionTip > 被移除

大多数脚手架已经正确配置环境了,没有必要.

  • Vue.config.ignoredElements

app.config.isCustomElement替代

  • Vue.component

app.component

  • Vue.directive

app.directive替代

  • Vue.mixin

app.mixin替代,强烈推荐使用组合API代替mixin

  • Vue.prototype

app.config.globalProperties替代

  • Vue.extend > 被移除

其实可以使用defineComponentdefineAsyncComponent来替代

21. 全局API Treeshaking(没看懂,先放着)
22. 移除内联模版Attribute

vue2中

<my-component inline-template>
  <div>
    <p>它们将被编译为组件自己的模板,</p>
    <p>而不是父级所包含的内容。</p>
  </div>
</my-component> 
23. key Attribute

在vue2中,需要手动添加key,相当于给一个唯一标识.提高效率;

在vue3中,会自动添加key;key应该被设置在<template>中.

24. 按键修饰符
  • 不支持数字作为v-on的修饰符
  • 不支持config.keyCodes

在vue3中,可以使用kebab-cased或者使用符号代替

  <!-- 回车键提交  -->
  <input v-on:keyup.enter='submit'>
  <!-- 会同时匹配Q和q -->
  <input v-on:keyup.q='quit'> 

关于符号问题,除了",',/,=,>,和.之外,其他估计都可以直接用符号代替

<input v-on:keypress.,='commaPress'>

25. 移除$listeners

listenersr融合到listenersr融合到listenersrattrs中

  <!-- vue2 -->
  <input type="text" v-bind="$attrs" v-on="$listeners" />
  <!-- vue3 -->
  <input type="text" v-bind="$attrs"/>
26. 挂载元素问题

vue2中,被挂载元素会被替代

vue3中,挂载的模板会渲染到被挂载元素中,当作innerHTML

27. 移除propsData

在vue2中,可以使用propsData给props传值

  const Home = Vue.extend({
    props:['title'],
    render(h){return h('div',title)}
  });
  new Home({
    propsData:{
      title:'NEW PASSAGE'
    }
  })

在vue3中,可以使用createApp的第二个参数:

import { createApp , h } from 'vue'; 
const Home = createApp({
  props:['title'],
  render(){return h('div',title)}
},{
  title:'NEW PASSAGE'
})
18. prop的默认函数中不能访问this
  • 组件接收到原始的prop将作为参数传递给默认函数
  • inject API可以在默认函数中使用
import { inject } from 'vue';
export default{
  props:{
    theme:{
      default(props){
        return inject('theme','default-theme');
      }
    }
  }
}
19.Provide/Inject

provide(name,value)用于上层组件向下抛出变量

  // 常适用于于组合API中
  import { provide } from 'vue';
  export default{
    setup(){
      provide('id','flevena');  //provide(抛出的名字,抛出的值)
      provide('score',{
        math:90,
        english:90,
        average:90
      })
    }
  }

inject(name[,default-value])用于下层组件接收上层组件抛出的变量。以此实现了跨级组件的通信。

 import { inject } from 'vue';
 export default{
   setup(){
     const studentId = inject('id',1);    // 接收抛出的id,并将其值设为1
     const studentScores = inject('score');
     return {
       studentId,
       studentScores
     }
   }
 }

为了使得provide值和inject值之间具有响应性,可以在provide值时候,使用ref或reactive

  import { provide , reactive , ref } from 'vue';
  export default{
    setup(){
      const location = ref('North Pole');
      const geolocation = reactive({
        longitude:90,
        latitude:135
      })

      provide('location',location);
      provide('geolocation',geolocation);
    }
  }

但是provide的值最好只能在提供者组件内部进行修改,其他子组件最好只能阅读。这样确保了数据的私密性。【java的setter、getter逻辑】

 import { provide , reactive , ref , readonly } from 'vue'; 
 export default{
   setup(){
     const location = ref('North Pole');
     const geolocation = reactive({
       longitude:90,
       latitude:135
     });
    // 提供修改的方法
    const updateLocation = (newLocation) =>{
      location.value = newLocation;
    }

    // 抛出变量和方法
    provide('location',readonly(location));
    provide('geolocation',readonly(geolocation));
    provide('updateLocation',updateLocation);
   }
 }
20. 渲染函数API

h变成了全局导入import { h } from 'vue',不再作为render的参数

由于render不再接收参数,所以一般h用于setup()内部。因为可以访问作用域内声明的响应式状态和函数以及setup()的参数

 import { h, reactive } from 'vue';
 export default{
   setup(props,{solts,attrs,emit}){
     const state = reactive({
       count:0,
     });

     function increment(){
       state.count++;
     }

     //返回渲染函数
     return ()=>{
       h('div',{
         onClick:increment
       },state.count);
     }
   }
 }
21. 注册组件

vue2中

  Vue.component('new-button',{});
  export default(){
    render(h){
      return h('new-button');
    }
  }

vue3中,由于VNode是上下文无关的,所以不能用字符串找到组件。需要resolveComponent方法解决

 import { h, resolveComponent } from 'vue';
 export default{
   setup(){
     const newButton = resolveComponent('new-button');
     return ()=>h(newButton);
   }
 }
22. 插槽统一
  • this.$slots,插槽作为函数公开.
  • 移除this.$scopedSlots

在vue2中,

  //在LayoutComponent中,创建两个作用域插槽,名为header和content。
  h(LayoutComponent,[
    h('div',{slot:'header'},this.header),
    h('div',{slot:'content'},this.content)
  ])
  // 引用作用域插槽:
  this.$scopedSlots.header;

vue3中

  h(LayoutComponent,{},{
    header:()=> h('div',this.hader),
    content:()=> h('div',this.content)
    // vue3引用
    this.$slots.header();
  })
23. 过渡的class名更改了

v-enter 改为 v-enter-from
v-leave 改为 v-leave-from

v-enter-active思路:从v-enter-fromv-enter-to

  .v-enter-from,
  .v-leave-to{
    opacity:0
  }

v-leave-active思路:从v-leave-fromv-leave-to

  .v-leave-from,
  .v-enter-to{
    opacity:1
  }
24. Transition

<transition>作为更根节点的组件,从外部切换时将不再触发过渡效果。

25. Transition Group

<transition-group> 不再默认渲染根元素,但仍然可以用tag attribute 创建根元素。

26. 移除v-on.native

可以使用**新增选项emits**定义允许被触发的事件

27. v-ifv-for的优先级问题

vue2中,v-for 优先于 v-if

vue3中,v-if 优先于 v-for

28. v-bind合并行为

绑定顺序会影响渲染结果

  • vue2:
  <!-- 模板 -->
  <div id='red' v-bind="{id:'blue'}"></div>
  <!-- 结果 -->
  <div id='red'></div>
  • vue3:具有顺序的,后面会覆盖前面
  <!-- 模板 -->
  <div id='red' v-bind="{id:'blue'}"></div>
  <!-- 结果 -->
  <div id='blue'></div>
29. hook: 改为了 vnode-
  <!-- vue2 -->
  <child-component @hook:updated='onUpdated'>
  <!-- vue3 -->
  <child-component @vnode-updated='onUpdated'>
30. 侦听数组

侦听数组的时候,只有数组被替代才会触发回调函数。

在vue3中,添加deep:true选项,数组改变就会触发回调函数

  watch: {
  bookList: {
    handler(val, oldVal) {
      console.log('book list changed')
    },
    deep: true
  },
}

Composition API

特点:
  1. Composition API是基于函数的,可以有效地组织和编写可重用代码(可以通过provide inject 共享变量、函数)
  2. 通过将共享逻辑分离为功能来提高代码的可读性
  3. 实现代码分离
  4. 在Vue中更好地使用TypeScript
setup() 选项实现组合式API;组件创建前,props被解析后,setup就会被作为组合式API入口
  • 注意一下几点:

    1. 由于进入setup()的时候,组件还未被创建,所以不能通过this获取datacomputedmethodsrefs的property
    2. setup()是一个接收propscontext的函数
    3. setup()返回的内容,会暴露给组件的其他部分(计算属性、方法、生命周期钩子等待)组件的模板
  • props,是setup()的第一个参数

    1. props的是响应式的,有新值,就会被更新
    2. 因为props是响应式的,因此不能被ES6解构
    3. 如果需要对props进行结构,需要引入并使用toRefs(props)
  • context,是setup()的第二个参数(包含attrs slots emits expose

    1. context是一个普通的JavaScript对象,里面包含:
      • attrs
      • slots
      • emit
      • expose(后面会分析)
    2. context是一个对象,所以可以解构
    3. 建议对context进行解构,方便以attrs.xslots.x的形式引用property.
    4. 不能对解构的attrs等,再进行解构。
  • 在模板中,使用setup()中返回的值,不需要.value,因为在模板中访问时是被自动浅解包

  • setup()的返回内容,可以是内部定义的值、方法。还可以是渲染函数。

  import { ref , h } from 'vue';
  export default {
    setup(props,context){
      const count =ref(0);
      const increment = ()=> ++count.value;
      return ()=> {
        h('div',count.value); 
      }
    }
  }

问题来了,要是返回的是一个渲染函数,里面的函数怎样通过模板暴露给父组件?答案就是:expose函数

  import { ref , expose , h } from 'vue';
  export default {
    setup(props,context){
      const count =ref(0);
      const increment = ()=> ++count.value;
      expose({
        increment,  // 这个 increment 方法现在将可以通过父组件的模板 ref 访问。
      })
      return ()=> {
        h('div',count.value); 
      }
    }
  }
ref()reactive() 都可以将setup()中的数据创建一个响应式引用。变量的传递变成了传递引用
  • ref() 推荐定义基本数据类型
  • reactive() 推荐定义复杂的数据类型
setup() 内生命周期钩子

相较于选项里面的钩子,setup()中只需要加一个on就可以

本来应该还有beforeCreate created,由于setup()正处于这两个周期中,所以在setup()中被取消了。而其中的逻辑代码可以写在setup()中。

  • onBeforeMount
  • onMounted
  • onBeforeUpdate
  • onUpdated
  • onBeforeUnmount
  • onUnmounted
  • onErrorCaptured
  • onRenderTracked
  • onRenderTriggered
  • onActivated
  • onDeactivated

这些函数接收一个回调函数 callFunction,当钩子被组件调用的时候,该回调函数会被执行。

watch响应式更改
  • 注意以下几点:
    1. watch需全局导入
    2. watch接收三个参数
      • 需要侦听的响应式引用或getter函数
      • 回调函数
      • 可选的配置选项【deep:true】
    3. 侦听单个数据源
  import { ref , watch } from 'vue';
  export default{
    setup(){
      const count = ref(0);
      watch(count,(newValue,oldValue)=>{
        // 进行操作
      })
    }
  }
  1. 侦听多个数据源【使用数组】
  const firstName = ref('')
  const lastName = ref('')

  watch([firstName, lastName], (newValues, prevValues) => {
    console.log(newValues, prevValues)
  })

  // 单个修改
  firstName.value = 'John' // logs: ["John", ""] ["", ""]
  lastName.value = 'Smith' // logs: ["John", "Smith"] ["John", ""]
  // 一次性多个修改
  const changeValues = ()=>{
    firstName.value = 'John'
    lastName.value = 'Smith' 
    // 打印 ["John", "Smith"] ["", ""]
  }
  1. 侦听数组、对象

由于数据是响应的,要求有一个由值构成的副本

  const numbers = reactive([1,2,3,4]);
  watch(
    ()=>[...numbers], //getter函数
    (numbers,prevNumbers)=>{
      console.log(numbers,prevNumbers);
    }
  )
  numbers.push(5);  //logs: [1,2,3,4,5] [1,2,3,4]

对于深度嵌套对象或数组,需要添加deep:true的选项

  const state = reactive({ 
    id: 1,
    attributes: { 
      name: '',
    }
  })

  watch(  // 无效,没有deep不能修改深度嵌套的对象
    () => state,  //getter函数
    (state, prevState) => {
      console.log('not deep', state.attributes.name, prevState.attributes.name)
    }
  )

  watch(  // 有效
    () => state,  //getter函数
    (state, prevState) => {
      console.log('deep', state.attributes.name, prevState.attributes.name)
    },
    { deep: true }
  )

  state.attributes.name = 'Alex' // 日志: "deep" "Alex" ""

为了完全深度侦听深度嵌套的对象或数组,需要使用外部工具进行深度拷贝

  import _ from 'lodash'
  const state = reactive({
    id:1,
    attributes:{
      name:'',
    }
  });
  watch(
    () => _.cloneDeep(state),   //深度拷贝
    (state,prevState) =>{
      console.log(state.attributes.name, prevState.attributes.name);
    }
  )
  state.attributes.name = 'Alex' // 日志: "Alex" "" 
computed函数

允许在组件外部创建计算属性。

  import { ref , computed } from 'vue';
  const count = ref(0);
  const twiceTheCount = computed( ()=> count.value * 2 );
  coute.value++;
  console.log(count.value); //1
  console.log(twiceTheCount.value); //2
组合API的总结

个人理解,其实就是把组件内的选项化为函数,将然后将一些通用的共享的一些数据、方法提取出来。实现代码的复用、分离。
通过一个类似于钩子函数的setup()进行整合,抛出。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐