vue3使用指南

vue3使用指南

主要介绍vue3的使用,同时会涉及到vue2,并会讲解其中的一些差异。

安装

CDN引入

如果想快速体验,可以直接通过cdn进行引入。

<div id="app">{{msg}}</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script >
  Vue.createApp({
    data() {
      return {
        msg: 'Vue3'
      }
    }
  }).mount('#app')
</script>

通过 CDN 引入 Vue 时,由于不涉及到构建步骤,可以使得设置更加简单,并且可以用于增强静态的 HTML 或与后端框架集成。但是这也意味着无法使用SFC(单文件)语法。

使用es模块构建

现代浏览器大多都已原生支持 ES 模块,可以像这样通过 CDN 以及原生 ES 模块使用 Vue

<div id="app">{{msg}}</div>
<script type="module">
  import { createApp } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js'
  createApp({
    data() {
      return {
        msg: 'Vue3'
      }
    }
  }).mount('#app')
</script>
使用import maps
<div id="app">{{msg}}</div>
<script type="importmap">
  {
    "imports": {
      "vue": "https://unpkg.com/vue@3/dist/vue.esm-browser.js"
    }
  }
</script>
<script type="module">
  import {createApp} from 'vue'
  createApp({
    data() {
      return {
        msg: 'Hello Vue!'
      }
    }
  }).mount('#app')
</script>

工程化

使用vue@latest方式

Vue 官方的项目脚手架工具,内部使用的是vite进行处理。

npm init vue@latest

这个指令会安装并执行create-vue,之后按照提示安装依赖并运行即可。
在这里插入图片描述
请注意,生成的项目中的示例组件使用的是组合式 API<script setup>,而非选项式 API

使用@vue/cli方式

虽然vue现在已经不推荐使用该方式,内部使用的webpack进行处理,但是依旧提供了vue3模板的下载。

安装@vue/cli

npm i @vue/cli -g

# 创建项目
vue create xxx

在这里插入图片描述
可以选择官方的vue3模板,也可以自行配置。
在这里插入图片描述

使用vite创建
# npm 6.x
npm init vite@latest <project-name> --template vue
# npm 7+,需要加上额外的双短横线
npm init vite@latest <project-name> -- --template vue

在这里插入图片描述

创建实例

vue2中,通过引入vue文件,进行实例的创建。

import Vue from 'vue'

new Vue({
  ...
})

而在vue3中做了改变,被移动到了由新的 createApp 方法所创建的应用实例上。

import { createApp } from 'vue'

const app = createApp({})

组合式API

vue2.x中,一个组件中通常包含了datacomputedmethodswatch等组件选项来组织逻辑,当组件变得越来越大时,逻辑关注点的列表也会增长,尤其对于那些一开始没有编写这些组件的人来说,这会导致组件难以阅读和理解。

这种碎片化使得理解和维护复杂组件变得困难。选项的分离掩盖了潜在的逻辑问题。此外,在处理单个逻辑关注点时,我们必须不断地“跳转”相关代码的选项块。

为了使得能将同一个逻辑的相关代码单独放在一起,vue3引入了组合式API的概念,使用setup来编写组件式API

来简单看看一个例子理解一下组合式API

<template>
  <!-- 计数器 -->
  <p>{{obj.count}}</p>
  <button @click="add">+1</button>

  <!-- v-if展示区域 -->
  <button @click="show">显示</button>
  <button @click="hide">隐藏</button>
  <div v-if="showDivFlag">一个被控制显隐的div</div>
</template>
<script>
import { reactive, ref } from 'vue'
// 计数器逻辑
function count() {
  const data = {
    count: 0
  }
  const obj = reactive(data);
  function add() {
    obj.count++;
  }
  return {
    obj,
    add
  }
}
// v-if展示区域逻辑
function vifDiv() {
  const showDivFlag = ref(true)
  function show() {
    showDivFlag.value = true
  }
  function hide() {
    showDivFlag.value = false
  }
  return {
    showDivFlag,
    show,
    hide
  }
}
export default {
  setup() {
    const { obj, add } = count();
    const {showDivFlag, show, hide} = vifDiv();
    return {
      obj,
      add,
      showDivFlag,
      show,
      hide
    }
  },
}
</script>

setup内对数据进行响应式,之后将datamethods返回,这样,模板中就可以使用这些。比起vue2中选项式API,使用setup能将各个模块逻辑单独放在一起,避免在methods中定义一大堆的方法。

以上例子使用vue2的选项式API

<template>
  <!-- 计数器 -->
  <p>{{obj.count}}</p>
  <button @click="add">+1</button>

  <!-- v-if展示区域 -->
  <button @click="show">显示</button>
  <button @click="hide">隐藏</button>
  <div v-if="showDivFlag">一个被控制显隐的div</div>
</template>
<script>
export default {
  data() {
    return {
      obj: {
        count: 0
      },
      showDivFlag: true
    }
  },
  methods: {
    add() {
      this.obj.count++;
    },
    show() {
      this.showDivFlag = true;
    },
    hide() {
      this.showDivFlag = false;
    }
  }
}
</script>

组合式 API 的核心思想是直接在函数作用域内定义响应式状态变量,并将从多个函数中得到的状态组合起来处理复杂问题。这种形式更加自由,也需要开发者对 Vue 的响应式系统有更深的理解才能高效使用。相应的,它的灵活性也使得组织和重用逻辑的模式变得更加强大。

两种 API 风格都能够覆盖大部分的应用场景。它们只是同一个底层系统所提供的两套不同的接口。实际上,选项式 API 是在组合式 API 的基础上实现的。关于 Vue 的基础概念和知识在它们之间都是通用的。

setup()

组件中的一个选项,处于生命周期函数 beforeCreate 钩子函数之前的函数,props 被解析之后执行。同时也是组合式API的入口。

接受两个参数:

  • props: 父组件的传值
    如果想在setup中使用props,必须在propsprop进行声明,未声明的prop的无法在setup中的props中解构出来(可以通过context.attrs获取)。声明了的prop即使父组件没有传,在setupprops仍然可以获取,但是值为undefined或者props中定义的默认值。
    props: {
      key: String,
      value: {
        type: String,
        default: 'default'
      }
    },
    setup(props) {
      console.log(props);
    }
    
    setup 函数中的 props 是响应式的,当传入新的 prop 时,它将被更新。但是,因为 props 是响应式的,你不能使用 ES6 解构,因为它会消除 prop 的响应性。可以使用toRefs来解构。
    setup(props) {
      const {key, value} = toRefs(props);
    }
    
  • context: 上下文, context 是一个普通的 JavaScript 对象,也就是说,它不是响应式的,这意味着你可以安全地对 context 使用 ES6 解构。
    context有4个属性:
    • attrs: 非响应式对象,跟vue2this.$attrs一样,用于接收父组件传的但是子组件中未声明的值。
    • emit: 跟vue2this.$emit一样 用于向父组件触发事件。
    • slots:非响应式对象,跟vue2this.$slots一样,用于获取插槽的信息。
    • expose:用于当在setup中使用渲染函数,而无法将组件中的data这些暴露给外部组件(外部组件通过ref获取)。
      // 子组件
      <script>
      import { h, ref } from 'vue'
      export default {
        setup(props, { expose }) {
          const count = ref(0)
          const increment = () => ++count.value
          expose({
            increment
          })
          return () => h('div', count.value)
        }
      }
      </script>
      
      <!-- 父组件 -->
      <son ref="son"></son>
      <script>
      export default {
        mounted() {
          console.log(this.$refs['son'].increment())
        }
      }
      </script>
      

执行 setup 时,组件实例尚未被创建(在 setup() 内部,this 不会是该活跃实例的引用,即不指向vue实例,Vue 为了避免开发者错误的使用,直接将 setup 函数中的this修改成了 undefined
setup() 中返回的对象上的 property 返回并可以在模板中被访问时,它将自动展开为内部值。不需要在模板中追加 .value
setup函数只能是同步的不能是异步的

<script setup>其实就是 setup 函数的一个语法糖。标签内的代码会被编译成setup()函数里的内容(这样也能使得组件实例在被创建的时候才会执行,而普通的<script>在组件引入时就执行)。

下面举几个例子看看语法糖的简便:

1. 变量和方法

<script setup>
  import {ref} from 'vue';
  import {minusNum} from 'xxx';
  const num = ref(1);
  function addNum() {
    num.value++;
  }
</script>

<script>
  import {ref} from 'vue';
  import {minusNum} from 'xxx';
  setup() {
    const num = ref(1);
    function addNum() {
      num.value++;
    }
    return {
      num,
      addNum,
      minusNum
    }
  }
</script>

使用语法糖不需要将变量和方法(包括外部文件中的方法)返回即可使用。

2. 组件注册、自定义指令

<template>
  <h1 v-direct>directive</h1>
</template>
<script setup>
  import { Component1 } from 'xxx';
  import { vDirect } from 'xxx';
</script>

<script>
  import { Component1 } from 'xxx';
  import { direct } from 'xxx';
  export default {
    components: {
      Component1
    },
    directives: {
      direct
    }
  }
</script>

使用语法糖组件就不需要在 component 注册,在组件中定义指令虽然也不需要在 directives 中注册,但是需要遵循 vNameOfDirective 这样的命名规范,否则并不会注册成指令。

3. 父子组件通信

<script setup>
  import { defineProps, defineEmits } from 'vue'
  const emit = defineEmits(['addNum']);
  const addNumEmit = () => {
    emit('addNum', 1);
  }

  const props = defineProps({
    num: {
      type: Number,
      default: 0
    }
  })

  console.log(props.num);
</script>

<script>
  export default {
    props: {
      num: {
        type: Number,
        default: 0
      }
    },
    setup (props, { emit }) {
      console.log(props.num)
      const addNumEmit = () => {
        emit('addNum', 1)
      }
      return {
        addNumEmit
      }
    }
  }
</script>
可以与普通script一起使用

<script setup> 可以和普通的 <script> 一起使用。

<script>
export default {
  mounted() {
    console.log('mounted');
  }
}
</script>
<script setup>
  import { onMounted } from 'vue';
  onMounted(() => {
    console.log('setup mounted')
  })
</script>

setup中定义的生命周期钩子比普通script先执行。

生命周期

setup中通过onXXX来注册生命周期:

选项式 API 的生命周期选项和组合式 API 之间的映射

因为 setup 是围绕 beforeCreatecreated 生命周期钩子运行的,因此setup内部的没有beforeCreatecreated对应的钩子映射。

  • beforeMount -> onBeforeMount
  • mounted -> onMounted
  • beforeUpdate -> onBeforeUpdate
  • updated -> onUpdated
  • beforeUnmount -> onBeforeUnmount
  • unmounted -> onUnmounted
  • errorCaptured -> onErrorCaptured
  • renderTracked -> onRenderTracked
  • renderTriggered -> onRenderTriggered
  • activated -> onActivated
  • deactivated -> onDeactivated

这些函数接受一个回调函数,当钩子被组件调用时将会被执行:

export default {
  setup() {
    // mounted
    onMounted(() => {
      console.log('Component is mounted!')
    })
  }
}

选项式API与组合式API混用

选项式API与组合式API是允许混用的(需要Vue3vue2.7后的版本)。如下:

<script>
  import { reactive } from 'vue';
  export default {
    data() {
      return {
        value: 1
      }
    },
    props: {
      num: {
        type: Number,
        default: 0
      }
    },
    setup() {
      let name = reactive('leo');
      return {
        name
      }
    },
    methods: {
      getName() {
        console.log(this.name);
      }
    }
  }
</script>

在混用的时候需要注意由于setup函数里的this指向undefined,因此不能使用选项式中定义的datamethods这些。

组件

组件注册

全局注册

vue2中是直接使用vue的原型中component进行注册.

import Vue from 'vue';
Vue.component('xxx', xxx);

vue3中,则是在实例中进行添加

import { createApp } from 'vue'

const app = createApp({})
app.component('xxx', xxx);
局部注册

前面提到了对于<script setup>不需要进行声明,直接引入即可使用,否则跟vue2一样也需要在components中注册。

<template>
  <Component1 />
</template>
<script setup>
  import { Component1 } from 'xxx';
</script>

<script>
  import { Component1 } from 'xxx';
  export default {
    components: {
      Component1
    }
  }
</script>

响应式API

reactive

reactive用于将数据变成响应式数据。调用reactive后返回的对象是响应式副本而非原始对象。其原理就是将传入的数据包装成一个Proxy对象。

import { reactive, watchEffect } from 'vue';
const data = {
  count: 0
};
const obj = reactive(data);
data === obj // false

watchEffect(() => {
  // 用于响应性追踪
  console.log(obj.count);
});

setTimeout(() => {
  obj.count++;
}, 2000);

响应式转换是“深层”的——它影响所有嵌套 property。在基于 ES2015 Proxy 的实现中,返回的 proxy 是不等于原始对象的。建议只使用响应式 proxy,避免依赖原始对象。

reactive用于复杂数据类型,比如对象和数组等,当传入基础数据类型时,默认情况下修改数据,界面不会自动更新,如果想更新,可以通过重新赋值的方式。

readonly

接受一个对象 (响应式或纯对象) 或 ref 并返回原始对象的只读代理。只读代理是深层的:任何被访问的嵌套 property 也是只读的,不能改变值,否则是报错。

import { reactive, watchEffect, readonly } from 'vue';
const data = {
  count: 0
};
const obj = reactive(data);
const copy = readonly(obj);

watchEffect(() => {
  // 用于响应性追踪
  console.log(obj.count)
});

setTimeout(() => {
  obj.count++;
  copy.count++;
}, 2000);

ref

接受一个内部值并返回一个响应式且可变的 ref 对象。ref 对象仅有一个 .value值,指向该内部值。跟reactive类似,也是将数据变成响应式。

import { ref, watchEffect } from 'vue';

const count = ref(0);
const obj = ref({
  count: 0
})
console.log(obj)
watchEffect(() => {
  console.log(count.value);
})
watchEffect(() => {
  console.log('obj.value.count: ', obj.value.count);
})

count.value++;
obj.value.count++;

ref和reactive的区别

  • ref是把值类型添加一层包装,使其变成响应式的引用类型的值。ref(0) --> reactive( { value:0 })
  • reactive 则是引用类型的值变成响应式的值。

两者的区别只是在于是否需要添加一层引用包装,对于对象而言,添加一层包装后会被reactive处理为深层的响应式对象,在调用unref后就能看到其实对象是一个Reactive对象

像上面的例子,使用ref同样可以将对象响应化,不过访问的时候需要调用value.去访问内部属性。所以对于对象而言,最好使用reative去响应化处理。

watch

vue3中组合式api watchvue2中选项式的watch完全等效。watch 需要监听特定的数据源,并在单独的回调函数中执行副作用。默认情况下,它也是惰性的——即回调仅在监听源发生变化时被调用。

watchEffect 相比,watch 允许我们:

  • 惰性地执行副作用;
  • 更具体地说明应触发监听器重新运行的状态;
  • 访问被监听状态的先前值和当前值。
监听单一源

源数据可以是一个reactive,也可以直接是一个 ref
语法

watch( name , callback, options )
  • name: 需要监听的属性或者返回值的getter函数
  • callback: 属性改变后执行的方法,接受两个参数
    • newVal: 新值
    • oldVal: 旧值
  • options: 配置项,可配置如下
    • deep: Boolean, 是否深度监听
    • immediate: Boolean,是否立即执行
// 侦听reactive
const state = reactive({ count: 0, value: 1 })
// 只监听对象中的count
watch(
  // 使用getter函数保证只监听了state中的count
  () => state.count,
  (count, prevCount) => {
    /* ... */
  }
)

// 侦听state中所有属性
watch(state, (newVal, oldVal) => {
  // ...
})

// 直接侦听一个 ref
const count = ref(0)
watch(count, (count, prevCount) => {
  /* ... */
})
监听多个源

使用数组来同时侦听多个源数据,当任一源数据发生改变都会触发watch

const state = reactive({ count: 0, value: 1 });
const count = ref(0)
watch([state, count], ([newState, newCount], [oldState, oldCount]) => {
  console.log(newState);
  console.log(newCount);
})

state.count++;
count.value++;
深度监听

watch的第三个参数中传入deep: true

const state = reactive({ count: 0, value: { status: false } });
watch(() => state, (newVal, oldVal) => {
  console.log('不会触发')
})

watch(() => state, (newVal, oldVal) => {
  console.log('触发深度监听')
}, {
  deep: true
})

state.value.status = true;

由于getter方法只返回state,对没有对内部的对象进行监听,因此内部对象的属性发生改变不会触发watch

当没有使用getter方法而是传入state这个reactive数据,则不需要设置deep: true都会进行深度监听。

立即执行

watch的第三个参数中传入immediate: true,当传入数据就会执行一次。

watchEffect

立即执行传入的一个函数,响应式追踪其依赖,在其依赖变更时重新运行该函数

它会监听引用数据类型的所有属性,不需要具体到某个属性,一旦运行就会立即监听,组件卸载的时候会停止监听

const count = ref(0)

watchEffect(() => console.log(count.value))
// -> logs 0

setTimeout(() => {
  count.value++
  // -> logs 1
}, 100)
停止监听

watchEffect 在组件的 setup() 函数或生命周期钩子被调用时,侦听器会被链接到该组件的生命周期,并在组件卸载时自动停止。

在一些情况下,也可以显式调用返回值以停止侦听

const stop = watchEffect(() => {
  /* ... */
})

// later
stop()
清除副作用

有时副作用函数会执行一些异步的副作用,这些响应需要在其失效时清除 (即完成之前状态已改变了) 。所以侦听副作用传入的函数可以接收一个 onInvalidate 函数作入参,用来注册清理失效时的回调。当以下情况发生时,这个失效回调会被触发:

  • 副作用即将重新执行时
  • 侦听器被停止 (如果在 setup() 或生命周期钩子函数中使用了 watchEffect,则在组件卸载时)
watchEffect(onInvalidate => {
  const token = performAsyncOperation(id.value)
  onInvalidate(() => {
    // id has changed or watcher is stopped.
    // invalidate previously pending async operation
    token.cancel()
  })
})

computed

接受一个 getter 函数,并根据 getter 的返回值返回一个不可变的响应式 ref 对象。

const count = ref(1)
const plusOne = computed(() => count.value + 1)

console.log(plusOne.value) // 2

plusOne.value++ // 错误

或者,接受一个具有 getset 函数的对象,用来创建可写的 ref 对象。

const count = ref(1)
const plusOne = computed({
  get: () => count.value + 1,
  set: val => {
    count.value = val - 1
  }
})

plusOne.value = 1
console.log(count.value) // 0