Vue3.0基础

188 阅读6分钟

Vue3基本概述

image.png

内容

2020年9月18日,Vue发布了3.0版本,代号:One Piece(海贼王),周边生态原因,当时大多数开发者还处于观望状态。

现在主流组件库都已经发布了支持Vue3.0的版本,列如 Element Plus、 Vant、 Vue Use,其他生态也不断地完善中,所以Vue3是趋势。

2022你哦按2月7日开始,Vue3也将成为新的默认版本。

优点

Composition Api , 能够更好的组织 、 封装 、 复用代码 、 RFCs。

性能:打包大小减少 41%、初次渲染快 55%、更新渲染快 133%、内存减少 54%,主要原因在于Proxy,VNode,Tree Shaking support。

Better TS support ,源码。

新特性: F绕过,emt 、 Teleport 、 Suspense。

趋势:未来肯定会有越来越对多的企业用Vue3.0 + TS 进行大型项目的开发。

对于个人来说:适应市场蓄妾,学习流行的技术提升竞争力,加薪!

Vite项目创建

Vite基本使用

内容

是下一代前端开发与构建工具,热更新、打包构建速度更快,但目前周边生态还不如Webpack成熟,所以实际开发中还是建议使用Webpack。

但目前就学习Vue3 语法来说,我们可以使用更轻量级的Vite。

如何构建

# yarn create vite-app + 项目名称
npm init vite-app + 项目名称
cd  项目名称
npm install
npm run dev
  • Webpack:会将所有模块提前编译、打包,不管这个模块是否被用到,随着项目越来越大,打包启动速度自然越来越慢。
  • Vite:瞬间开启一个服务,并不会先编译所有文件,当浏览器用到某个文件时,Vite 服务会收到请求然后编译后响应到客户端。

image.png

image.png

// 1. 导入 createApp 函数,不再是曾经的 Vue 了
// 2. 编写一个根组件 App.vue,导入进来
// 3. 基于根组件创建应用实例,类似 Vue2 的 vm,但比 vm 更轻量
// 4. 挂载到 index.html 的 #app 容器
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
app.mount('#app')

// Vue2: new Vue()、new VueRouter()、new Vuex.Store()
// Vue3: createApp()、createRouter()、createStore()
<template>
  <div class="container">我是根组件</div>
</template>
<script>
  export default {
    name: 'App'
  }
</script>

image.png

学习组合API

选项/组合API\

image.png

Vue2

优点;已于学习和使用,写代码的位置约定好。

缺点:数据和业务逻辑分散在同一个文件的N个地方,随着业务复杂度的上升,可能会出现动图左侧的代码组织方式,不利于管理和维护.

<template>
  <div class="container">
    <p>X 轴:{{ x }} Y 轴:{{ y }}</p>
    <hr />
    <div>
      <p>{{ count }}</p>
      <button @click="add()">自增</button>
    </div>
  </div>
</template>
<script>
  export default {
    name: 'App',
    data() {
      return {
        // !#Fn1
        x: 0,
        y: 0,
        // ?#Fn2
        count: 0
      }
    },
    mounted() {
      // !#Fn1
      document.addEventListener('mousemove', this.move)
    },
    methods: {
      // !#Fn1
      move(e) {
        this.x = e.pageX
        this.y = e.pageY
      },
      // ?#Fn2
      add() {
        this.count++
      }
    },
    destroyed() {
      // !#Fn1
      document.removeEventListener('mousemove', this.move)
    }
  }
</script>

ifer_options.gif 优点:可以把同一功能的 ***数据 *** 和 ***业务逻辑 *** 组织到一起,方便复用和维护。

缺点:需要良好的代码组织和拆分能力,相对没有Vue2 容易上手。

注意:为了能让大家较好的过滤到Vue3.0版本,目前也是支持Vue2.x选项API的写法。

链接: why-composition-apicomposition-api-doc。****

<template>
  <div class="container">
    <p>X 轴:{{ x }} Y 轴:{{ y }}</p>
    <hr />
    <div>
      <p>{{ count }}</p>
      <button @click="add()">自增</button>
    </div>
  </div>
</template>
<script>
  import { onMounted, onUnmounted, reactive, ref, toRefs } from 'vue'
  export default {
    name: 'App',
    setup() {
      // !#Fn1
      const mouse = reactive({
        x: 0,
        y: 0
      })
      const move = (e) => {
        mouse.x = e.pageX
        mouse.y = e.pageY
      }
      onMounted(() => {
        document.addEventListener('mousemove', move)
      })
      onUnmounted(() => {
        document.removeEventListener('mousemove', move)
      })

      // ?Fn2
      const count = ref(0)
      const add = () => {
        count.value++
      }

      // 统一返回数据供模板使用
      return {
        ...toRefs(mouse),
        count,
        add
      }
    }
  }
</script>

steup入口函数

内容:

setup是Vue3中新增的巨剑配置项,作为组合API的入口函数。

执行时机:实例创建前调用,升值遭遇Vue2中的 beforeCreate 。

注意点:由于执行setup的时候是咧还没有created,所以在setup中是不能直接使用的data和methods中的数据的,所以Vue3干脆把setup中的this绑定了undefined,防止乱用!

虽然Vue2中的data和methods配置虽然在Vue3中也能使用,但不建议了,建议数据和方法都卸载setup函数中,并通过return惊醒返回可在模板中直接使用(一般情况下setup不能为异步函数)。

<template>
  <h1 @click="say()">{{ msg }}</h1>
</template>
<script>
  export default {
    setup() {
      const msg = 'Hello Vue3'
      const say = () => {
        console.log(msg)
      }
      return { msg, say }
    }
  }
</script>

setup 也可以返回一个渲染函数(问:setup 中 return 的一定只能是一个对象吗?)

<script>
  import { h } from 'vue'
  export default {
    name: 'App',
    setup() {
      return () => h('h2', 'Hello Vue3')
    }
  }
    new Vue({render: h => h(App)})
</script>

reactive包装数组

内容:

reactive是一个函数,用来将普通对象/数组包装成响应式数据使用。

实列

image.png

<template>
  <ul>
    <li v-for="(item, index) in arr" :key="item" @click="removeItem(index)">
      {{ item }}
    </li>
  </ul>
</template>

<script>
  export default {
    name: 'App',
    setup() {
      const arr = ['a', 'b', 'c']
      const removeItem = (index) => {
        arr.splice(index, 1)
      }
      return {
        arr,
        removeItem
      }
    }
  }
</script>

数据缺失是删除了,但是试图没更新,不是响应式的!

所以可以用reactive包装数组

<template>
  <ul>
    <li v-for="(item, index) in arr" :key="item" @click="removeItem(index)">
      {{ item }}
    </li>
  </ul>
</template>

<script>
  import { reactive } from 'vue'
  export default {
    name: 'App',
    setup() {
      const arr = reactive(['a', 'b', 'c'])
      const removeItem = (index) => {
        arr.splice(index, 1)
      }
      return {
        arr,
        removeItem
      }
    }
  }
</script>

ref

基本使用

ref 函数,可以把简单数据类型包裹为响应式数据(复杂类型也可以),注意 JS 中操作值的时候,需要加 .value 属性,模板中正常使用即可。

<template>
  <div class="container">
    <div>{{ name }}</div>
    <button @click="updateName">修改数据</button>
  </div>
</template>
<script>
  import { ref } from 'vue'
  export default {
    name: 'App',
    setup() {
      const name = ref('ifer')
      const updateName = () => {
        name.value = 'xxx'
      }
      return { name, updateName }
    }
  }
</script>

包装复杂数据类型

注意:ref 其实也可以包裹复杂数据类型为响应式数据,一般对于数据类型未确定的情况下推荐使用 ref,例如后端返回的数据。

<template>
  <div class="container">
    <div>{{ data?.name }}</div>
    <button @click="updateName">修改数据</button>
  </div>
</template>
<script>
  import { ref } from 'vue'
  export default {
    name: 'App',
    setup() {
      // 初始值是 null
      const data = ref(null)
      setTimeout(() => {
        // 右边的对象可能是后端返回的
        data.value = {
          name: 'ifer'
        }
      }, 1000)
      const updateName = () => {
        data.value.name = 'xxx'
      }
      return { data, updateName }
    }
  }
</script>

toRef

内容

toRef 函数的作用:转换响应式对象中某个属性为单独响应式数据,并且转换后的值和之前是关联的(ref 函数也可以转换,但值非关联)。

image.png

<template>
  <div class="container">
    <h2>{{ name }}</h2>
    <button @click="updateName">修改数据</button>
  </div>
</template>
<script>
  import { reactive, toRef } from 'vue'
  export default {
    name: 'App',
    setup() {
      const obj = reactive({
        name: 'ifer',
        age: 10
      })
      const name = toRef(obj, 'name')
      const updateName = () => {
        // 注意:需要使用 name.value 进行修改
        name.value = 'xxx'
        // 对 obj.name 的修改也会影响视图的变化,即值是关联的
        // obj.name = 'xxx' // ok
      }
      return { name, updateName }
    }
  }
</script>

toRefs

内容

toRefs 函数的作用:转换响应式对象中所有属性为单独响应式数据,并且转换后的值和之前是关联的。

image.png

<template>
  <div class="container">
    <h2>{{ name }} {{ age }}</h2>
    <button @click="updateName">修改数据</button>
  </div>
</template>
<script>
  import { reactive, toRefs } from 'vue'
  export default {
    name: 'App',
    setup() {
      const obj = reactive({
        name: 'ifer',
        age: 10
      })
      const updateName = () => {
        obj.name = 'xxx'
        obj.age = 18
      }
      return { ...toRefs(obj), updateName }
    }
  }
</script>

computed

基本

作用:computed 函数用来定义计算属性。

<template>
  <p>firstName: {{ person.firstName }}</p>
  <p>lastName: {{ person.lastName }}</p>
  <input type="text" v-model="person.fullName" />
</template>
<script>
  import { computed, reactive } from 'vue'
  export default {
    name: 'App',
    setup() {
      const person = reactive({
        firstName: '朱',
        lastName: '逸之'
      })
      // 也可以传入对象,目前和上面等价
      person.fullName = computed({
        get() {
          return person.firstName + ' ' + person.lastName
        },
        set(value) {
          const newArr = value.split(' ')
          person.firstName = newArr[0]
          person.lastName = newArr[1]
        }
      })
      return {
        person
      }
    }
  }
</script>

watch

监听 reactive 内部数据

注意 1:监听 reactive 内部数据时,强制开启了深度监听,且配置无效;监听对象的时候 newValue 和 oldValue 是全等的。

<template>
  <p>{{ obj.hobby.eat }}</p>
  <button @click="obj.hobby.eat = '面条'">click</button>
</template>

<script>
  import { watch, reactive } from 'vue'
  export default {
    name: 'App',
    setup() {
      const obj = reactive({
        name: 'ifer',
        hobby: {
          eat: '西瓜'
        }
      })
      watch(obj, (newValue, oldValue) => {
        // 注意1:监听对象的时候,新旧值是相等的
        // 注意2:强制开启深度监听,配置无效
        console.log(newValue === oldValue) // true
      })

      return { obj }
    }
  }
</script>

注意 2:reactive 的【内部对象】也是一个 reactive 类型的数据。

<template>
  <p>{{ obj.hobby.eat }}</p>
  <button @click="obj.hobby.eat = '面条'">click</button>
</template>

<script>
import { watch, reactive, isReactive } from 'vue'
export default {
  name: 'App',
  setup() {
    const obj = reactive({
      name: 'ifer',
      hobby: {
        eat: '西瓜',
      },
    })
    // reactive 的【内部对象】也是一个 reactive 类型的数据
    // console.log(isReactive(obj.hobby))
    watch(obj.hobby, (newValue, oldValue) => {
      console.log(newValue === oldValue) // true
    })

    return { obj }
  },
}
</script>

注意 3:对 reactive 自身的修改则不会触发监听。

<template>
  <p>{{ obj.hobby.eat }}</p>
  <button @click="obj.hobby = { eat: '面条' }">click</button>
</template>

<script>
import { watch, reactive } from 'vue'
export default {
  name: 'App',
  setup() {
    const obj = reactive({
      name: 'ifer',
      hobby: {
        eat: '西瓜',
      },
    })
    watch(obj.hobby, (newValue, oldValue) => {
      // obj.hobby = { eat: '面条' }
      console.log('对 reactive 自身的修改不会触发监听')
    })
    return { obj }
  },
}
</script>

监听 ref 数据

监听一个 ref 数据

📝 监听 age 的变化,做一些操作。

<template>
  <p>{{ age }}</p>
  <button @click="age++">click</button>
</template>

<script>
  import { watch, ref } from 'vue'
  export default {
    name: 'App',
    setup() {
      const age = ref(18)
      // 监听 ref 数据 age,会触发后面的回调,不需要 .value
      watch(age, (newValue, oldValue) => {
        console.log(newValue, oldValue)
      })

      return { age }
    }
  }
</script>
监听多个 ref 数据

📝 可以通过数组的形式,同时监听 age 和 num 的变化。

<template>
  <p>age: {{ age }} num: {{ num }}</p>
  <button @click="handleClick">click</button>
</template>

<script>
  import { watch, ref } from 'vue'
  export default {
    name: 'App',
    setup() {
      const age = ref(18)
      const num = ref(0)

      const handleClick = () => {
        age.value++
        num.value++
      }
      // 数组里面是 ref 数据
      watch([age, num], (newValue, oldValue) => {
        console.log(newValue, oldValue)
      })

      return { age, num, handleClick }
    }
  }
</script>
立即触发监听
<template>
  <p>{{ age }}</p>
  <button @click="handleClick">click</button>
</template>

<script>
  import { watch, ref } from 'vue'
  export default {
    name: 'App',
    setup() {
      const age = ref(18)

      const handleClick = () => {
        age.value++
      }

      watch(
        age,
        (newValue, oldValue) => {
          console.log(newValue, oldValue) // 18 undefined
        },
        {
          immediate: true
        }
      )

      return { age, handleClick }
    }
  }
</script>
开启深度监听 ref 数据

解决 1:当然直接修改整个对象的话肯定是会被监听到的(注意模板中对 obj 的修改,相当于修改的是 obj.value)。

<template>
  <p>{{ obj.hobby.eat }}</p>
  <button @click="obj = { hobby: { eat: '面条' } }">修改 obj</button>
</template>

<script>
  import { watch, ref } from 'vue'
  export default {
    name: 'App',
    setup() {
      const obj = ref({
        hobby: {
          eat: '西瓜'
        }
      })
      watch(obj, (newValue, oldValue) => {
        console.log(newValue, oldValue)
        console.log(newValue === oldValue)
      })

      return { obj }
    }
  }
</script>

解决 2:开启深度监听 ref 数据。

watch(
  obj,
  (newValue, oldValue) => {
    console.log(newValue, oldValue)
    console.log(newValue === oldValue)
  },
  {
    deep: true
  }
)
  1. 解决 3:还可以通过监听 ref.value 来实现同样的效果。

🧐 因为 ref 内部如果包裹对象的话,其实还是借助 reactive 实现的,可以通过 isReactive 方法来证明。

<template>
  <p>{{ obj.hobby.eat }}</p>
  <button @click="obj.hobby.eat = '面条'">修改 obj</button>
</template>

<script>
  import { watch, ref } from 'vue'
  export default {
    name: 'App',
    setup() {
      const obj = ref({
        hobby: {
          eat: '西瓜'
        }
      })
      watch(obj.value, (newValue, oldValue) => {
        console.log(newValue, oldValue)
        console.log(newValue === oldValue)
      })

      return { obj }
    }
  }
</script>

监听普通数据

监听响应式对象中的某一个普通属性值,要通过函数返回的方式进行(如果返回的是对象/响应式对象,修改内部的数据需要开启深度监听)。

<template>
  <p>{{ obj.hobby.eat }}</p>
  <button @click="obj.hobby.eat = '面条'">修改 obj</button>
</template>

<script>
import { watch, reactive } from 'vue'
export default {
  name: 'App',
  setup() {
    const obj = reactive({
      hobby: {
        eat: '西瓜',
      },
    })
    // 把 obj.hobby 作为普通值去进行监听,只能监听到 obj.hobby 自身的变化
    /* watch(
      () => obj.hobby,
      (newValue, oldValue) => {
        console.log(newValue, oldValue)
        console.log(newValue === oldValue)
      }
    ) */
    // 如果开启了深度监听,则能监听到 obj.hobby 和内部数据的所有变化
    /* watch(
      () => obj.hobby,
      (newValue, oldValue) => {
        console.log(newValue, oldValue)
        console.log(newValue === oldValue)
      },
      {
        deep: true,
      }
    ) */
    // 能监听影响到 obj.hobby.eat 变化的操作,例如 obj.hobby = { eat: '面条' } 或 obj.hobby.eat = '面条',如果是 reactive 直接对 obj 的修改则不会被监听到(ref 可以)
    watch(
      () => obj.hobby.eat,
      (newValue, oldValue) => {
        console.log(newValue, oldValue)
        console.log(newValue === oldValue)
      }
    )
    return { obj }
  },
}
</script>

Vue3 生命周期

内容

  • 组合 API生命周期写法,其实 选项 API 的写法在 Vue3 中也是支持。
  • Vue3(组合 API)常用的生命周期钩子有 7 个,可以多次使用同一个钩子,执行顺序和书写顺序相同。
  • setup、onBeforeMount、onMounted、onBeforeUpdate、onUpdated、onBeforeUnmount、onUnmounted。
<template>
  <hello-world v-if="state.bBar" />
  <button @click="state.bBar = !state.bBar">destroy cmp</button>
</template>

<script>
  import HelloWorld from './components/HelloWorld.vue'
  import { reactive } from 'vue'
  export default {
    name: 'App',
    components: {
      HelloWorld
    },
    setup() {
      const state = reactive({
        bBar: true
      })
      return {
        state
      }
    }
  }
</script>
<template>
  <p>{{ state.msg }}</p>
  <button @click="state.msg = 'xxx'">update msg</button>
</template>

<script>
  import {
    onBeforeMount,
    onMounted,
    onBeforeUpdate,
    onUpdated,
    onBeforeUnmount,
    onUnmounted,
    reactive
  } from 'vue'
  export default {
    name: 'HelloWorld',
    setup() {
      const state = reactive({
        msg: 'Hello World'
      })

      onBeforeMount(() => {
        console.log('onBeforeMount')
      })
      onMounted(() => {
        console.log('onMounted')
      })
      onBeforeUpdate(() => {
        console.log('onBeforeUpdate')
      })
      onUpdated(() => {
        console.log('onUpdated')
      })
      onBeforeUnmount(() => {
        console.log('onBeforeUnmount')
      })
      onUnmounted(() => {
        console.log('onUnmounted')
      })
      return {
        state
      }
    }
  }
</script>

setup 函数参数

父传子

<template>
  <h1>父组件</h1>
  <p>{{ money }}</p>
  <hr />
  <!-- 1. 父组件通过自定义属性提供数据 -->
  <Son :money="money" />
</template>
<script>
  import { ref } from 'vue'
  import Son from './Son.vue'
  export default {
    name: 'App',
    components: {
      Son
    },
    setup() {
      const money = ref(100)
      return { money }
    }
  }
</script>
<template>
  <h1>子组件</h1>
  <p>{{ money }}</p>
</template>
<script>
  export default {
    name: 'Son',
    // 2. 子组件通过 props 进行接收,在模板中就可以使用啦
    props: {
      money: {
        type: Number,
        default: 0
      }
    },
    setup(props) {
      // 3. setup 中也可以通过形参 props 来获取传递的数据
      console.log(props.money)
    }
  }
</script>

子传父

<template>
  <h1>父组件</h1>
  <p>{{ money }}</p>
  <hr />
  <Son :money="money" @change-money="updateMoney" />
</template>
<script>
  import { ref } from 'vue'
  import Son from './Son.vue'
  export default {
    name: 'App',
    components: {
      Son
    },
    setup() {
      const money = ref(100)
      // #1 父组件准备修改数据的方法并提供给子组件
      const updateMoney = (newMoney) => {
        money.value -= newMoney
      }
      return { money, updateMoney }
    }
  }
</script>
<template>
  <h1>子组件</h1>
  <p>{{ money }}</p>
  <button @click="changeMoney(1)">花 1 元</button>
</template>
<script>
  export default {
    name: 'Son',
    props: {
      money: {
        type: Number,
        default: 0
      }
    },
    emits: ['change-money'],
    setup(props, { emit }) {
      // attrs 捡漏、slots 插槽
      const changeMoney = (m) => {
        // #2 子组件通过 emit 进行触发
        emit('change-money', m)
      }
      return { changeMoney }
    }
  }
</script>

provide/inject

📝 把 App.vue 中的数据传递给孙组件,Child.vue。

<template>
  <div class="container">
    <h2>App {{ money }}</h2>
    <button @click="money = 1000">发钱</button>
    <hr />
    <Parent />
  </div>
</template>
<script>
  import { provide, ref } from 'vue'
  import Parent from './Parent.vue'
  export default {
    name: 'App',
    components: {
      Parent
    },
    setup() {
      // 提供数据
      const money = ref(100)
      provide('money', money)
      // 提供修改数据的方法
      const changeMoney = (m) => (money.value -= m)
      provide('changeMoney', changeMoney)
      return { money }
    }
  }
</script>
<template>
  <div>
    Parent
    <hr />
    <Child />
  </div>
</template>

<script>
  import Child from './Child.vue'
  export default {
    components: {
      Child
    }
  }
</script>
<template>
  <div>
    Parent
    <hr />
    <Child />
  </div>
</template>

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

Vue3 其他变更

v-model

在 Vue2 中 v-mode 指令语法糖简写的代码。

<Son :value="msg" @input="msg=$event" />

在 Vue3 中 v-model 语法糖有所调整。

<Son :modelValue="msg" @update:modelValue="msg=$event" />

<template>
  <h2>count: {{ count }}</h2>
  <hr />
  <Son :modelValue="count" @update:modelValue="count = $event" />
  <!-- <Son v-model="count" /> -->
</template>
<script>
  import { ref } from 'vue'
  import Son from './Son.vue'
  export default {
    name: 'App',
    components: {
      Son
    },
    setup() {
      const count = ref(10)
      return { count }
    }
  }
</script>
<template>
  <h2>子组件 {{ modelValue }}</h2>
  <button @click="$emit('update:modelValue', 100)">改变 count</button>
</template>
<script>
  export default {
    name: 'Son',
    props: {
      modelValue: {
        type: Number,
        default: 0
      }
    }
  }
</script>

ref 属性

内容

获取单个 DOM。

<template>
  <!-- #3 -->
  <div ref="dom">我是box</div>
</template>
<script>
  import { onMounted, ref } from 'vue'
  export default {
    name: 'App',
    setup() {
      // #1
      const dom = ref(null)
      onMounted(() => {
        // #4
        console.log(dom.value)
      })
      // #2
      return { dom }
    }
  }
</script>

获取组件实例。

<template>
  <!-- #4 -->
  <button @click="changeName">修改子组件的 Name</button>
  <hr />
  <!-- #3 -->
  <Test ref="test" />
</template>
<script>
  import { ref } from 'vue'
  import Test from './Test.vue'
  export default {
    name: 'App',
    components: {
      Test
    },
    setup() {
      // #1
      const test = ref(null)
      const changeName = () => {
        test.value.changeName('elser')
      }
      // #2
      return { test, changeName }
    }
  }
</script>
<template>
  <div>
    <p>{{ o.name }}</p>
  </div>
</template>

<script>
  import { reactive } from 'vue'
  export default {
    setup() {
      const o = reactive({ name: 'ifer' })

      const changeName = (name) => {
        o.name = name
      }
      return {
        o,
        changeName
      }
    }
  }
</script>

Teleport

作用

传送,能将特定的 HTML 结构(一般是嵌套很深的)移动到指定的位置,解决 HTML 结构嵌套过深造成的样式影响或不好控制的问题。

<template>
  <div class="child">
    <teleport to="body">
      <dialog v-if="bBar" />
    </teleport>
    <button @click="handleDialog">显示弹框</button>
  </div>
</template>
<script>
  import { ref } from 'vue'
  import Dialog from './Dialog.vue'
  export default {
    name: 'Child',
    components: {
      Dialog
    },
    setup() {
      const bBar = ref(false)
      const handleDialog = () => {
        bBar.value = !bBar.value
      }
      return {
        bBar,
        handleDialog
      }
    }
  }
</script>