Skip to content

官网:Vue.js - 渐进式 JavaScript 框架 | Vue.js (vuejs.org)

V3

V3 工程可以通过 vue-cli 脚手架进行创建,但是现在流行用 create-vue 脚手架进行创建 V3 工程

create-vue:这个脚手架是基于 vite 项目构建工具来实现的(vue-cli 是通过 webpack 项目构建工具来实现的)

vite 的特点,一个字 快,服务器启动快,修改代码之后重新响应也很快(vite 也是尤雨溪团队开发的)

要使用 vue-cli 创建 V3 工程,那么 vue-cli 的版本要求最低 4.5.0,可以使用 vue --version 查看版本,如果版本较低可以通过 npm install -g @vue-cli 来进行升级

Vite 脚手架官网:Vite中文网 (vitejs.cn)

创建工程

官方指导:快速上手 | Vue.js (vuejs.org)

在工程目录下使用创建命令:npm create vue@latest(安装vue-create脚手架并创建V3工程,如果脚手架存在的话不会重新安装)

vite 与 webpack 创建的工程有何不同

入口:index 文件放在了 public 文件夹外面,以 index.html 作为入口,不在以 main.js 作为入口

配置文件:对于 vite 构建工具来说,配置文件是 vite.config.js 他与 webpack 中的 vue.config.js 类似,配置文档 配置 Vite | Vite 官方中文文档 (vitejs.dev)

setup

setup是一个函数,V3中新增的配置项,组件中所用到的data、methods、钩子等都要配置到 setup 中

vue
<template>
  <h1>{{ name }}</h1>
  <button @click="getName">输出名字</button>
</template>

<script>
export default {
  name: 'App',
  setup() {
    // 定义的变量相当于 data
    let name = '123'
    // 定义的函数相当于 methods
    function getName() { alert(name) }
    // 想要使用对应的数据必须进行返回 以 K:V 的方式,如果返回名称一致直接使用变量名也行
    return {name, getName}
  }
}
</script>

响应式

如果直接在方法中进行更改变量,那么是没有响应式的,也就是说,数据是改了但是不会更新主屏幕

ref 实现

javascript
// 导入 
import {ref} from "vue";

export default {
  setup() {
    // 定义的变量使用 ref 进行报错
    let name = ref('123')

    // 在使用或者更改的时候使用以下方式
    name.value = '456'
    
    return {name}
  }
}

在模板语法中可以直接使用,不需要 .value

当 data 是对象的时候,可以采让 ref 包裹住对象的方式

javascript
import {ref} from "vue";
export default {
  setup() {
    // 使用 ref 包裹住对象
    let user = ref({
      name: '张三',
      age: 18
      hobby: ["足球", "篮球", "羽毛球"]
    })
	// 当调用方法是进行响应式更改
    function getName() {
      // 使用以下方式进行更改
      user.value.name = '李四'
      user.value.age = 19
      user.value.hobby[0] = "乒乓球"
    }

    return {user, getName}
  }
}

ref 适合基本数据类型

reactive 实现

reactive 是一个函数,这个实现不能用于基本数据类型,这个函数是专门用于对象类型的

javascript
import {reactive} from "vue";

export default {
  setup() {
    // 使用 reactive 包裹
    let user = reactive({
      name: '张三',
      age: 18,
      hobby: ["足球", "篮球", "羽毛球"]
    })

    function getName() {
      // 在修改的时候不需要使用 value 就可以直接修改
      user.name = '李四'
      user.age = 19
      user.hobby[0] = "乒乓球"
    }

    return {user, getName}
  }
}

使用以上的方式也可以对盒子套盒子的对象进行改变,并且对于 中途添加一个属性、中途删除一个属性(使用 delete 对象.属性)、通过数组下标直接访问 也是可以响应式的

props

父组件

vue
<template>
  <User :age="user.age" :name="user.name"/>
</template>

<script>
import User from "@/components/User.vue";

export default {
  name: 'App',
  // 导入组件
  components: {User},
  setup() {
    let user = {
      name: '张三',
      age: 18,
    }

    return {user}
  }
}
</script>

子组件

vue
<template>
  <h1>{{ user.name }} - {{ user.age }}</h1>
</template>

<script>
export default {
  name: "User",
  // 需要声明 props
  props: ["name", "age"],
  setup(props) {
    return {user: props}
  }
}
</script>

V3在调用 setup 函数之前会传递参数,第一个是 props。它被包装成了一个代理对象,具有响应式的 proxy 对象

defineProps

这种方式是Vue3使用语法糖之后的写法,具体为以下

javascript
// 导入 — 这很重要
import {defineProps} from 'vue';
// 使用 defineProps 方法并使用链表的方式传入要接收的数据
const props = defineProps(['content']);

生命周期

跟V2的声明周期作比较

  • 生命周期最后两个钩子由原来的 beforeDestorydestoryed 变为 beforeUnmountunmounted
  • setup 函数执行在 beforeCreate 之前

在V3中生命周期钩子函数由两种写法:选项式、组合式

选项式API:在 setup 外面写生命周期钩子(V2的写法)

组合式API:在 setup 中写生命周期钩子,并且缺少 beforeCreatebeforeed 因为 setup 代替了(如果想使用这两个钩子的话,只能采用选项式的写法)。

javascript
// 从这里导入对应的钩子
import {onBeforeMount} from "vue";

export default {
  // ...
  setup() {
    onBeforeMount(() => {
      // ...
    });
    // ...
  }
}

自定义事件

父组件

vue
<template>
  <!-- 创建自定义事件 -->
  <User @event="log"/>
</template>

<script>
import User from "@/components/User.vue";

export default {
  name: 'App',
  components: {User},
  setup() {
    // 事件触发时输出内容
    function log(name, sex) {
      alert(`姓名:${name},性别:${sex}`)
    }

    return {log}
  }
}
</script>

子组件

vue
<template>
  <button @click="trigger">点击触发自定义事件</button>
</template>

<script>
export default {
  name: "User",
  setup(props, context) {
    function trigger() {
      // 触发自定义事件 并传参
      context.emit("event", "张三", "男")
    }

    return {trigger}
  }
}
</script>

setup 的第二个参数是组件的上下文对象,然后使用 emit 触发事件,第一个是事件的名称,往后所有都是传递的参数

全局事件总线

V3的全局事件总线需要借助第三方库 mitt

安装

安装:npm i mitt

然后封装一个 src/util/event-bus.js 的文件

javascript
import mitt from "mitt";
export default mitt();

使用

父组件

vue
<template>
  <User/>
</template>

<script>
import User from "@/components/User.vue";
import {onMounted} from "vue";
// 导入全局事件总线对象
import emitter from './util/event-bus'

export default {
  name: 'App',
  components: {User},
  setup() {
    // 生命周期钩子 mounted
    onMounted(() => {
      // 给全局事件总线绑定事件 | 绑定事件 - 触发的函数
      emitter.on("event", (user) => {
        console.log(user.name, user.sex)
      })
    })
  }
}
</script>

子组件

vue
<template>
  <button @click="trigger">点击触发自定义事件</button>
</template>

<script>
import emitter from '../util/event-bus'

export default {
  name: "User",
  setup(props, context) {
    function trigger() {
      // 触发自定义事件 并传参
      emitter.emit("event", {
        name: "张三",
        sex: "男"
      })
    }

    return {trigger}
  }
}
</script>

在以上的代码中通过全局事件总线实现的事件的绑定与触发。但是,与自定义事件不同的是,全局事件总线传递的参数只有一个,并且也不需要在组件上进行 @事件名

在使用是需要导入总线对象

javascript
import emitter from './util/event-bus'

绑定事件:emitter.on("事件名称", 回调函数)

触发事件:emitter.emit("事件名称", 参数)

解绑事件(全部):emitter.all.clear()

解绑事件(指定):emitter.off("事件名")

计算属性

在V3中计算属性是组合式写法,也就是得写在 setup 中

javascript
import {computed} from "vue";

export default {
  name: 'App',
  setup() {
    // 简写
    let com = computed(() => {
		return xxx;
    })

    // 完整写法
    let com = computed({
      set(value) { ... },
      get() { return xxx }
    })
    
    // 返回
    return {com}
  }
}

简写:如果只需要使用计算属性的 get 方法的情况下,就可以使用简写

完整:这种写法是包含 get/set 的方法的适合在需要直接改变计算属性的情况下使用

侦听属性

被监视的属性必须具有响应式才能够监听

侦听 ref

vue
<template>
  <input type="text" v-model="name"/>
</template>

<script>
import User from "@/components/User.vue";
import {ref, watch} from "vue";

export default {
  name: 'App',
  components: {User},
  setup() {
    let name = ref("张三")
    // 参数:被侦听的属性 回调函数 配置信息
    watch(name, (newValue, oldValue) => {
      console.log(`老数据:${oldValue} -> 新数据:${newValue}`)
    }, {
      // 立即执行
      immediate: true,
      // 深度监听
      deep: true
    })
    return {name}
  }
}
</script>

在上面的 watch 写法中,如果由多个要被侦听的属性就需要写多个 watch 但是,如果某些属性执行的逻辑是相同的,可以采用数组的形式进行简写

javascript
watch([name, age], (newValue, oldValue) => {
  console.log(`老数据:${oldValue} -> 新数据:${newValue}`)
}, {
  // 立即执行
  immediate: true,
  // 深度监听
  deep: true
})

因为采用的是数组,那么新数据和旧数据打印的也是数组,需要获取某个属性的新老数据时,采用数组的形式获取

当 ref 包裹的是对象的时候,监视属性的位置如果填写:xxx.value ,就相当于下面的 reactive 深度监视是开启的,但是如果直接填写 xxx 那就可以控制深度监视(默认关闭),想要监视单一属性就需要使用函数进行返回

javascript
// 默认不开启深度监视
watch(user, (newValue, oldValue) => {
  console.log(`老数据:${oldValue.name} -> 新数据:${newValue}`)
}, {deep: true})

watch(user.value, (newValue, oldValue) => {
  console.log(`老数据:${oldValue.name} -> 新数据:${newValue}`)
})

watch(() => user.value.name, (newValue, oldValue) => {
  console.log(`老数据:${oldValue.name} -> 新数据:${newValue}`)
})

侦听 reactive

javascript
let user = reactive({
  name: "张三",
  age: 18
})

watch(user, (newValue, oldValue) => {
  console.log(oldValue, newValue)
})

在侦听 reactive 函数获取的代理对象时,只能获取最新数据(newValue)老数据(oldValue)无法获取。并且对于代理对象来说,默认开启深度监视,而且这种深度监视是不可关闭的

如果想要进行单一的属性,不能直接 对象.属性 这种事错误的,应该采用一个函数把需要侦听的属性进行返回

javascript
watch(() => user.name, (newValue, oldValue) => {
  console.log(`老数据:${oldValue} -> 新数据:${newValue}`)
})

但是对于 对象.属性 是错误的也不是很对,在有一种场景下可以使用这种方式,那就是这个属性也是一个对象

javascript
let user = reactive({
  name: "张三",
  age: 18,
  xxx: {
  	a: 1,
  	b: 2
  }
})

在以上的情况下是可以直接 user.xxx 进行监听的,但是这种方式的老数据也是不能拿到的

在 reactive 中如果逻辑相同的话也是可以通过数组的形式来进行减少代码的,在 [] 中也是需要写函数的形式

watchEffect

这是 V3 新增的组合式API,作用就是返回监控数据变化。用法在里面跟上回调函数

javascript
setup() {
  let user = reactive({
    name: "张三",
    age: 18,
  })

  watchEffect(() => {
    const n = user.name
  })

  return {user}
}

回调函数的执行时机,按照上面的写法,每当 user.name 发生变化时,就会执行回调函数,如果有多个也是同理。但是 age 的变化不会让回调函数执行

自定义钩子函数(hook)

这个自定义钩子与V2中的 mixin 有点相同都是为了让代码得到复用

使用:在 src 目录下创建一个 hooks 的目录,然后在其中创建 js 文件(文件名一般是功能名)

javascript
// 这是一个简单的求和函数
export default function (a, b) {
    return a + b;
}

在使用的位置进行导入即可正常使用

javascript
import sum from "@/hooks/sum.js";

浅层次的响应式(shallow)

shallowReactive:对象第一层支持响应式,第二层就不再支持了

shallowRef:只给基本数据类型添加响应式,如果是对象,则不会给它添加响应式

深只读与浅只读

组合式API:readonly 和 shallowReadonly

场景:如果从其他组件传递过来的数据不希望修改,那么最好加上一个只读

深只读:具有响应式的对象中所有的属性,包括子对象中的子对象中的属性,它的所有值都是只读的不可修改的

使用方法只需要对要被只读的数据用 readonly 进行包裹

浅只读:具有响应式的对象第一层的值是只读的

响应式数据的判断

isRef:检查是否为ref对象

isReactive:检查是否是由 reactive() 或者 shallowReadonly() 创建的代理

isProxy:检查一个值是否是由 reactive()readonly()shallowReactive()shallowReadonly() 创建的代理

isReadonly :检查传入的值是否为只读对象

使用:从 vue 中导入相对应的内容然后直接使用即可

toRef 与 toRefs

在使用插值语法的的时候输入某些值时前面需要写很长的属性(xxx.xxx.xxx.age),如果要省略这些 xxx 那么可以在 return 返回的时候使用键值对的方式返回

javascript
let user = reactive({
  name: "IKun",
  age: 3,
  hobby: {
    h1: "唱",
    h2: "跳",
    h3: "rap",
    h4: "打篮球"
  }
})

return {
  h1: user.hobby.h1,
  h2: user.hobby.h2,
  h3: user.hobby.h3,
}

toRef

按照以上的方式就可以直接使用 h1 进行访问内容了,但是这种方法没有响应式,也就是说,更改 h1 的值 user 中的值不会更改。也可以直接使用 ref 再次进行包裹(h1: ref(user.hobby.h1))这种方式倒是有响应式,但是相当于新建的一个响应式对象,更改的也是这个新的,不会对老的 user 进行更改

在这种情况下就可以使用 toRef 进行包裹住,这样返回的数据不仅仅是老数据的而且也具有响应式

javascript
return {
  h1: toRef(user.hobby, "h1"),
  h2: toRef(user.hobby, "h2"),
  h3: toRef(user.hobby, "h3"),
  h4: toRef(user.hobby, "h4"),
}

toRef 的使用的两个参数,第一个参数是对象,第二个参数是对象中的属性名

toRefs

上面的方式有点过于繁琐,所以又有一个叫 toRefs 的内容

javascript
return {
  ...toRefs(user.hobby)
}

在 toRefs 中填入 user 的话,那么会给 user 的所有内容做代理。使用时,需要从当前层次进行使用,如上所示,在上面的写法中只给 user 内的 hobby 做了代理,使用时只能使用 hobby 内的数据,使用时直接使用属性名即可,如果单纯给 user 做代理,那么想要访问 h1 就得写成 hobby.h1 的形式

由于 toRefs 返回的是对象,所以采用 es6 语法(...xxx)进行添加返回

以上方式对 ref 和 reactive 都有相同的效果

(转换 | 标记)为原始

(转换 | 标记)为原始意思就是把响应式数据(转换 | 标记)成普通对象或者数据

转换(toRaw):将响应式对象转换为普通对象。只适用于reactive 生成的响应式对象

标记(markRaw):标记某个对象,让这个对象永远都不具备响应式

使用:直接对对象进行包裹并接收即可

Fragment 组件

Fragment翻译:碎片、片段

在V2中每个组件必须有一个根标签,在组件嵌套组件的时候性能问题,因为有些时候没必要去嵌套这些根标签。然后再V3中没有根标签或者说有多个根标签,实际上再所有标签嵌套完成后会再最外层添加一个 Fragment 用这个当根标签

Teleport 组件

Teleport翻译:远距离传送、瞬间移动。用于设置组件的显示位置

举个例子,再组件套组件中如果想要对最下层的某一个盒子做定位,那么定位是要有参照物的默认时body,但是如果某一层的组件添加上的某个属性作为了参照物,那么就会导致效果混乱,而 Teleport 组件就是把搬来应该再最下层的盒子移动到指定的位置,比如body的子盒子

vue
<template>
  <Teleport to="[标签名|ID选择器]">
    <User />
  </Teleport>
</template>

provide & inject 传递数据

在这种数据传递方式中,适合在祖宗组件对孙子组件传递数据的场景

javascript
// 先声明
provide("名称", 数据);

// 取值
let aaa = inject("名称");

自定义 ref

javascript
import {customRef} from "vue";

export default {
  name: 'App',
  setup() {
    // 自定义 ref
    function customizeRef(value) {
      // 返回需要是一个响应式的 ref
      return customRef((track, trigger) => {
        // 自定义 ref 的 get 和 set 并返回
        return {
          get() {
            // 追踪依赖
            track()
            return value;
          },
          set(newValue) {
            value = newValue
            // 触发依赖
            trigger()
          }
        }
      })
    }

    let hello = customizeRef("你好,世界");

    return {hello}
  }
}

通过以上的方式进行自定义一个 ref 这样做的目的是在输入和返回数据的时候进行夹带点私货

setup 语法糖

只需要把标签加入setup <script setup> 这样

javascript
export default {
  name: "User",
  components: {xxx},
  setup(props, context) {
    return {xxx}
  }
}

那么像以上这样的代码就不需要写了,组件导入之后不需要注册直接用

props

对于加了语法糖之后就没有了获取 props 内容的地方,vue提供了一个函数 defineProps() 只需要在这里面声明发送过来的属性名即可使用

javascript
// 数组形式
defineProps(["name", "age"]);