Vue3到底发生了哪些变化?

前言

Vue3距离现在2021-11-28已经发布差不多一年时间,市面上现在还是主要使用的vue2,那工作的伙伴是不是还在踌躇要不要学Vue3?此篇文章中并没有告诉你答案,作者现在也是处于一个学习研究Vue3的一个阶段,只会罗列出Vue3到底是发生了哪些变化,与有没有跟我们带来惊喜......相信看完这篇文章你们自有判断

你知道的越多,你不知道的越多 点赞再看,手留余香,与有荣焉

image.png 有一说一这张图我是非常满意的

一.Vue3的生态和优势

  • 社区生态 - 逐步完善
  • 整体优化 - 性能优化/TS支持优化/组合式API加持
  • 市场使用 - 部分技术选型激进的公司已经在生产环境使用了vue3

社区生态

组件(插件)名称 官方地址 简介
ant-design-vue antdv.com/docs/vue/in… ant-design-vue 是 Ant Design 的 Vue 实现,组件的风格与 Ant Design 保持同步
element-plus element-plus.gitee.io/#/zh-CN Element Plus,一套为开发者、设计师和产品经理准备的基于 Vue 3.0 的桌面端组件库
vant vant-contrib.gitee.io/vant/v3/#/z… 有赞前端团队开源的移动端组件库,于 2016 年开源,已持续维护 4 年时间
Naive UI www.naiveui.com/zh-CN/ 一个 Vue 3 组件库比较完整,主题可调,使用 TypeScript,不算太慢,有点意思
VueUse vueuse.org/ 基于composition组合api的常用集合

整体优化

  1. 性能提升

    • 首次渲染更快
    • diff算法更快
    • 内存占用更少
    • 打包体积更小
  2. 更好的Typescript支持

  3. Composition API (重点)

相关阅读:

  1. Vue3 中文文档 vue3js.cn/docs/zh/
  2. Vue3 设计理念 vue3js.cn/vue-composi…

市场使用

潜力很大,还没完全火起来

二. Vue3开发环境搭建

  1. 全局安装vue-cli
npm i -g @vue/cli
复制代码
  1. 创建脚手架
vue create vue3-demo(名称自定义)
复制代码

image.png 我选择的默认vue3的版本,可以自定义哈

  1. vue3的调试工具需要自己去下载额外的插件,把vue2的调试工具给禁用了才可以使用,双手奉上作者一直使用的插件

插件地址

image.png

  1. 以上步骤回车之后,vue-cli会帮助我们跑起来一个内置了vue3版本的vue项目

首先我们可以看一下package.json文件,在dependencies配置项中显示,我们当前使用的版本为3.2.0

"dependencies": {
    "vue": "^3.2.0"
}
复制代码

然后打开main.js 入口文件,发现Vue的实例化发生了一些变化,由先前的new关键词实例化,转变为createApp方法的调用形式 (更多详情:www.bilibili.com/read/cv1013…

vue2.x

new Vue({
  el: '#app',
  render: h => h(App)
})
复制代码

vue3.x

import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')
复制代码

最后我们打开一个单文件组件发现,vue3.0的单文件组件中不再强制要求必须有唯一根元素, 可以放任意多个根元素, 将 template 中的根元素全部以 appendChild 方式加入 #app, 不是替换

<template>
  <img alt="Vue logo" src="./assets/logo.png">
  <HelloWorld msg="Welcome to Your Vue.js App"/>
</template>
复制代码

但是呢,eslint会报错滴,需要在vscode找到vuter点开小齿轮-》扩展设置-》

image.png

image.png 把这个框框取消勾选就阔以啦

三.Vue3中的两种API

组合式API(Composition API)算是vue3对我们开发者来说非常有价值的一个API更新,我们先不关注具体语法,先对它有一个大的感知

image.png

1. vue2中的选项式API

<template>
  <p>
    <button @click="show">显示</button>
    <button @click="hide">隐藏</button>
  </p>
  <div class="id" v-if="isShow">我是一个显示隐藏的div</div>
  <p>
    <button @click="changeWhite">白色</button>
    <button @click="changeBlue">蓝色</button>
  </p>
  <div class="id" :style="{color}">我是改变文字颜色的div</div>
</template>

<script>
export default {
  name: "",
  data() {
    return {
      isShow: true,
      color:''
    };
  },
  methods: {
    show() {
      this.isShow = true;
    },
    hide() {
      this.isShow = false;
    },
    changeWhite(){
      this.color = '#fff'
    },
    changeBlue(){
      this.color = 'blue'
    }
  }
};
</script>

<style scoped >
.id {
  width: 200px;
  height: 200px;
  background-color: red;
}
</style>

复制代码

image.png

2. 组合式API

<template>
  <p>
    <button @click="show">显示</button>
    <button @click="hide">隐藏</button>
  </p>
  <div class="id" v-if="isShow">我是一个显示隐藏的div</div>
  <p>
    <button @click="changeWhite">白色</button>
    <button @click="changeGreen">绿色</button>
  </p>
  <!-- <div class="id" :style="`color:${changeColor}`">我是一个修改文字颜色的div</div> -->
  <div class="id" :style="`color:${color}`">我是一个修改文字颜色的div</div>
</template>

<script>
// ref 响应式,简单与复杂数据类型 (必须.value)
import { ref } from "vue";
// 功能一:盒子显示与隐藏
function useShow() {
  const isShow = ref(true);
  function show() {
    isShow.value = true;
  }
  function hide() {
    isShow.value = false;
  }
  return { isShow, show, hide };
}
// 功能二:文字颜色
function useChangeColor(){
  const color = ref('')
  function changeWhite(){
    color.value = '#fff'
  }
  function changeGreen(){
    color.value = 'green'
  }
  return {color,changeWhite,changeGreen}
}
export default {
  name: "App",
  setup() {
    const { isShow, show, hide } = useShow();
    const {color,changeWhite,changeGreen} =useChangeColor()
    return {
      isShow,
      show,
      hide,
      color,
      changeWhite,
      changeGreen
    };
  }
};
</script>

<style scoped>
.id {
  width: 200px;
  height: 200px;
  background-color: red;
}
</style>

复制代码

3.总结

综合上面两个写demo可以得到以下几点

  1. vue2中你会发现逻辑会很乱,所有的方法都写在methods中,如果data中数据越来愈多,你会发现找数据非常困难
  2. vue3中采用组合式API(Composition API)看上述代码你会发现,代码越写越麻烦了,但是逻辑思维非常清晰,可以单独封装,逻辑复用, 让功能的代码可以集中抽取到一个函数中

四.setup组合式API

1.setup入口函数

(1)主要内容

  1. setup 函数是一个新的组件选项,作为组件中组合式API 的起点(入口函数)
  2. setup 函数只会在组件初始化的时候执行一次
  3. setup 函数在beforeCreate生命周期钩子执行之前执行,实例还没生成,没有this
  4. Vue2 如果修改数组的元素 list[0] = 1 页面不会更新, Vue3 解决了该问题

(2)代码演示

export default {
  setup () {
    console.log('setup执行了')
    console.log(this)
  },
  beforeCreate() {
    console.log('beforeCreate执行了')
    console.log(this)
  }
}
复制代码

2.响应式API

(1)reactive

作用

reactive是一个函数,接收一个普通的对象传入,把对象数据转化为响应式对象并返回,用于声明响应式数据的函数

  • 参数: 传入数据(必须是引用数据类型)
  • 返回值: 实现了响应式的数据

注意: reactive 必须传入引用数据类型的数据, 不能做基本数据类型的响应式操作

示例:

<template>
  <div>
    {{ msg }}
    <button @click="msg = '张三'">点我改 msg</button>
    <p v-for="item in list" :key="item">{{ item }}</p>
    <button @click="list.push(4)">点我添加数据</button>
    <button @click="list[0] = '张三'">点我改索引0的数据</button>
  </div>
</template>

<script>
// 1. 导入 reactive 函数
import { reactive } from 'vue'
export default {
  setup() {
    // 直接声明变量不是响应式的数据, 修改数据时页面不会更新
    // let list = [1, 2, 3]

    // 2. 在 setup 函数中调用 reactive 函数
    // 传入一个引用数据类型的数据, 会将该数据变成响应式的
    let list = reactive([1, 2, 3])

    let msg = reactive('消息')

    // 3. setup 的返回值是一个对象, 将数据放入对象中返回
    return {
      list,
      msg
    }
  },
};
</script>
复制代码

(2)ref

作用

ref是一个函数,接受一个简单类型或者复杂类型的传入并返回一个响应式且可变的 ref 对象

  • 参数: 传入数据 所有数据类型都支持
  • 返回值: 实现了响应式的数据

注意: 在 setup 中使用数据时必须要 .value

示例:

<template>
  {{ msg }}
  <button @click="msg = '张三'">点我改 msg</button>
  <button @click="changeMsg">点我在 setup 中改 msg</button>
  <button @click="getMsg">点我在 setup 中取 msg</button>
  <p v-for="item in list" :key="item">{{ item }}</p>
  <button @click="list.push(4)">点我添加数据</button>
  <button @click="list[0] = '张三'">点我改索引0的数据</button>
  <button @click="changeList">点我在 setup 中改索引0的数据</button>
</template>

<script>
// 1. 导入 ref 函数
import { ref, reactive } from "vue";
export default {
  setup() {
    // 直接声明变量不是响应式的数据, 修改数据时页面不会更新
    // let list = [1, 2, 3]

    // 2. 在 setup 函数中调用 ref 函数
    let list = ref([1, 2, 3]);

    let msg = ref("消息"); // 将数据放到对象的 value 属性上, 从而实现响应式的效果

    console.log("list:", list);
    console.log("msg:", msg);

    function changeMsg() {
      console.log("changeMsg"
      // msg = '李四' // 必须加 .value
      msg.value = "李四";
    }

    function getMsg() {
      console.log(msg.value);
    }

    function changeList() {
      list.value[0] = "王五";
    }

    // 3. setup 的返回值是一个对象, 将数据放入对象中返回
    return {
      list,
      msg,
      changeMsg,
      getMsg,
      changeList,
    };
  },
};
</script>
复制代码

(3)ref响应函数与reactive响应函数的区别

两个函数都是用于声明响应式数据的函数

  • ref支持基本数据类型与引用数据类型
  • reactive只支持引用数据类型

注意: ref 其实就是对 reactive 的一层包装,内部调用 reactive 生成一个响应式的对象, 在外面包一个对象

(4)computed

1.作用

根据现有响应式数据经过一定的计算得到全新的数据

  • 参数1: 回调函数, 就是计算属性的 get 函数
  • 返回值: 计算属性

2.示例

<template>
  完整数组: {{ list }}
  <br />
  所有偶数: {{ evenList }}

  <button @click="evenList = '111'">点我修改计算属性</button>
</template>

<script>
// 1. 导入 computed 函数
import { ref, computed } from "vue";
export default {
  setup() {
    const list = ref([1, 2, 3, 4, 5, 6]);

    // 2. 在 setup 中调用 computed 函数来生成计算属性
    // const evenList = computed(() => {
    //   return list.value.filter(item => item % 2 === 0)
    // })

    // 完整写法, 将传入的函数改为 对象 即可
    const evenList = computed({
      get() {
        return list.value.filter((item) => item % 2 === 0);
      },
      set(val) {
        console.log(val)
      },
    });

    // 3. 返回计算属性
    return {
      list,
      evenList,
    };
  },

 
};
</script>
复制代码

3.vue2写法

 // Vue 2 的写法
  computed: {
     evenList: {
       get() {
         return '数据'
       },
       set(val) {}
     }
     evenList() { // 计算属性的 get 函数
       return '测试数据'
     }
  },
复制代码

(5)watch

1.作用

基于响应式数据的变化执行回调逻辑,和vue2中的watch的功能一致

  1. 普通监听
  2. 立即执行
  3. 深度监听
  • 参数1: source 源 要监听的数据, 传入函数
  • 参数2: 数据源发生变化后触发的函数, 传入函数
  • 参数3: 配置对象 (deep, immediate)

2.示例:

<template>
  总数: {{ count }}
  <button @click="count++">点我加加</button>
  <hr />
  {{ list }}
  <button @click="list.push(4)">点我添加数组的数据</button>
</template>

<script>
// 目标: 使用 watch 侦听 count 的变化
// 1. 导入 watch 函数
import { ref, watch } from "vue";
export default {
  setup() {
    const count = ref(0);

    // 2. 在 setup 函数中调用 watch 函数
    // watch(() => {
    //   // 返回你要监听的数据
    //   // 必须加 value
    //   return count.value
    // }, (newVal) => {
    //   // 数据变化时会触发
    //   console.log(newVal)
    // })

    // 不推荐使用, 容易产生理念冲突, 因为不用加 .value
    // watch(count, (newVal) => {
    //   // 数据变化时会触发
    //   console.log(newVal)
    // })

    // 深度侦听: 参数 3 传入一个配置对象
    const list = ref([1, 2, 3]);
    watch(
      () => { // 参数1: source 源 要监听的数据, 传入函数
        return list.value;
      },
      () => { // 参数2: 数据源发生变化后触发的函数, 传入函数
        console.log(list.value);
      },
      { // 参数3: 配置对象 (deep, immediate)
        deep: true, // 深度侦听
        immediate: true, // 立即侦听, 页面加载时执行一下侦听函数
      }
    );

    return {
      count,
      list,
    };
  },

 
};
</script>
复制代码

3.vue2写法

  Vue 2 计算属性写法
   watch: {
   1.简单写法
    // list(newVal) {}
   2.完整写法
     list: {
     // 复杂数据类型开启深度侦听
     deep: true,
     // 开启立即侦听
     immediate: true,
    handler() {}
     }
   }
复制代码

3.生命周期函数

(1)社区翻译的vue3生命周期图示

Vue3生命周期.jpeg

(2)在 setup 函数中使用生命周期钩子函数

选项式API下的生命周期函数使用 组合式API下的生命周期函数使用
beforeCreate 不需要(直接写到setup函数中)
created 不需要(直接写到setup函数中)
beforeMount onBeforeMount
mounted onMounted
beforeUpdate onBeforeUpdate
updated onUpdated
beforeDestroy Vue 3: beforeUnmount onBeforeUnmount
destroyed Vue 3: unmounted onUnmounted

注意啦:

  • vue3 的组合式API中没有提供初始化阶段的两个钩子函数: onBeforeCreate onCreated
  • 因为 Vue3 认为这两个阶段要做的事情, 都可以在 setup 里面直接做

(3) 生命周期钩子函数使用场景

生命周期钩子函数 应用场景
created 发送ajax请求 / 挂载共用属性
mounted 发送ajax请求 / 依赖于dom的业务,比如地图,图表
destroyed -> unmounted 销毁操作,比如定时器

4.父子通信

在vue3的组合式API中,父传子的基础套路完全一样,基础思想依旧为:父传子是通过prop进行传入,子传父通过调用自定义事件完成

  1. 父组件中给子组件传递数据, 属性绑定
  2. 在子组件中定义 props 接收并使用

props 特点:

  • 单向数据流, 单向下行绑定(父亲变了儿子会同步, 但儿子改数据 老子不认)
  • 如果传入的是引用数据类型, 修改对象的属性是可行的, 但是 ESLint 默认会检查并报错, 所以需要单独配置或关闭 ESLint

如何关闭eslint

配置vue.config.js

module.exports={
   lintOnSave:false
}
复制代码

反正我是不会关的

实现步骤

  1. setup函数提供俩个参数,第一个参数为props,第二个参数为一个对象context
  2. props为一个对象,内部包含了父组件传递过来的所有prop数据,context对象包含了attrs,slots, emit属性,其中的emit可以触发自定义事件的执行从而完成子传父

示例:

// setup 钩子函数中有两个参数:
  // 参数1: props 父组件传过来的数据都会挂载到这个对象上
  // 参数2: context 上下文对象, 有一些全局的实例方法, 例如 emit slots attrs
  // setup(props, context) {
  setup(props, { emit }) {
    // 以前使用 props 数据: this.msg
    // 现在没有 this 了, 如果要在 setup 中使用 props 的数据, 则需要接受第一个参数: props
    // console.log(props)
    // console.log(context)

    // this // 没有 this
    function sendValue() {
      // 在此处触发自定义事件, 携带数据给父组件
      // 这里的 this 是指 setup 返回的对象, 所以没有办法调用 $emit 函数
      // 故而无法在此处使用 this.$emit() 触发事件, 怎么办呢?
      // console.log(this.$emit) 
      // 参数1: 事件名
      // 参数2~n: 要传递的参数
      emit('getValue', '这是儿子孝敬您的数据')

      // 为什么 Vue2 的实例方法都是 $ 开头? 
      // 因为 Vue2 设计时所有的方法都放到原型上
      // data 和 methods 的成员也会被挂载到对象本身 this.msg
      // 如果原型上的成员不用 $ 开头, 很容易命名冲突, 命名冲突之后的结果
      // this.$set this.$nextTick() this.$emit() this.$mount()

      // Vue3 就没有这样设计了, 以上下文对象的形式暴露给用户使用, compositionAPI设计的出现, 让我们更少的依赖 data 和 methods 了


    }
    return {sendValue}
复制代码

5.provide 和 inject

1. 使用场景

通常我们使用props进行父子之间的数据传递,但是如果组件嵌套层级较深,一层一层往下传递将会变的非常繁琐,有没有一种手段可以把这个过程简化一下呢,有的,就是我们马上要学习的provide 和 inject,它们配合起来可以方便的完成从顶层组件向任意底层组件传递数据的效果

2. 基础使用

需求:爷组件中有一份数据 传递给孙组件直接使用

实现步骤

  1. 顶层组件在setup方法中使用provide函数提供数据

    provide('key',数据)

  2. 任何底层组件在setup方法中使用inject函数获取数据

    const data = inject('key')

落地代码

爷爷组件 - app.vue

<template>
  <father></father>
</template>

<script>
import Father from '@/components/Father'
import { provide } from 'vue'
export default {
  components: {
    Father
  },
  setup() {
    let name = '张三'
    // 使用provide配置项注入数据 key - value
    provide('name', name)
  }
}
</script> 
复制代码

孙组件 - components/Son.vue

<template>
  我是子组件
  {{ name }}
</template>

<script>
import { inject } from 'vue'
export default {
  setup() {
    const name = inject('name')
    return {
      name
    }
  }
}
</script>
复制代码

3. 传递响应式数据

provide默认情况下传递的数据不是响应式的,也就是如果对provide提供的数据进行修改,并不能响应式的影响到底层组件使用数据的地方,如果想要传递响应数据也非常简单,只需要将传递的数据使用ref或者reactive生成即可

<template>
  <father></father>
  <button @click="changeName">change name</button>
</template>

<script>
import Father from '@/components/Father'
import { provide, ref } from 'vue'
export default {
  components: {
    Father
  },
  setup() {
    // 使用ref转换成响应式再传递
    let name = ref('张三')
    function changeName(){
      name.value = 'pink'
    }
    provide('name', name)
    return {
      changeName
    }
  }
}
</script> 
复制代码

4.用法

image.png

6.TemplateRef

背景知识

在模板中使用ref,我们都很清楚,它一般有三种使用场景

  1. ref + 普通dom标签 获取真实dom对象
    this.$refs.box
  2. ref + 组件标签 获取组件实例对象 this.$refs.form.validate()
  3. ref + v-for 获取由dom对象(实例对象)组成的数组 (不经常使用

实现步骤

  1. 使用ref函数传入null创建 ref对象 => const hRef = ref(null)
  2. 模板中通过定义ref属性等于1中创建的ref对象名称建立关联 => <h1 ref="hRef"></h1>
  3. hRefreturn出去
  4. 使用 => hRef.value
<template>
  <!-- Vue2 的做法 -->
  <!-- <p ref="cuteP">这是一个可爱的 p</p>
  <Father ref="dad"></Father> -->
  
  <!-- 3. 在需要获取对象的标签上使用 ref 绑定返回的数据 -->
  <p ref="cp">这是一个可爱的 p</p>
  <Father ref="father"></Father>
</template>

<script>
import { onMounted, ref } from 'vue';
// 目标: 在 setup 函数中使用 ref 获取原生 DOM 对象
import Father from './components/Father.vue'
export default {
  components: {
    Father
  },

  setup() {
    // 1. 使用 ref 创建一个空的数据 用于后面产生关联
    const cp = ref(null)
    const father = ref(null)

    // 此时无法获取, 都是 null, 因为 setup 在 beforeCreate 之前执行
    console.log(cp.value)
    console.log(father.value)

    onMounted(() => {
      // 4. 在合适的时机使用 数据.value 即可访问原生 DOM
      // 在原生 DOM 挂载完毕后执行
      console.log(cp.value)
      console.log(father.value)
    })

    // 2. 将数据返回出去供 template 使用
    return {
      cp,
      father
    }
  }

  // mounted() {
  //   // Vue2 的做法: 必须在 mounted 中获取
  //   console.log(this.$refs.cuteP)
  //   console.log(this.$refs.dad)
  // }
};
</script>
复制代码

image.png

五. 非兼容语法

vue3.0对于2.0版本的大部分语法都是可以兼容的,但是也有一些破坏性的语法更新,这个大家要格外注意

1.实例方法$on移除 (eventBus现有实现模式不再支持 可以使用三方插件替代)

event Bus
 1. Vue.prototype.$eventBus = new Vue()
 2. 接收数据的组件里  this.$eventBus.$on('get-msg',(msg)=>{ })
 3. 发送数据的组件里  this.$eventBus.$emit('get-msg','传递的数据')
 
vue3中默认情况下eventBus模式是不支持的  
使用三方插件替代
复制代码

2.过滤器filter移除 (插值表达式里不能再使用过滤器 可以使用methods替代)

filter过滤器
字符串的格式化  方法 接收原字符串 返回格式化之后的字符串

{{ msg | formatMsg }}

vue3直接移除了该语法  可以直接使用methods替代 

{{ formatMsg('this is msg') }}  // 渲染的结果是什么呢? 函数return值
methods:{
  formatMsg(msg){
     return msg + 'zs'
  }
}
复制代码

3.sync语法移除 (和v-model语法合并)

.sync语法
elementUI  -> Dialog  visible.sync="showFlag"
作用: 简化父子通信 自定义事件的触发
.sync -> v-model(重点)
复制代码

更多非兼容语法变更请移步这里

如果内容有错误的地方欢迎指出(觉得看着不理解不舒服想吐槽也完全没问题);如果有帮助,欢迎点赞和收藏,转载请著明出处,如果有问题也欢迎私信交流

猜你喜欢

转载自juejin.im/post/7035805730372321294