Skip to content

Vue3 快速上手

注意:本文可能为了突出重点,有些例子是代码片段,而不是完整的代码。

改变❗

  • v-on 不再支持使用数字作为修饰符。❌
  • $on$off$once 实例方法被移除。❌
  • 过滤器 filters 移除不再支持,建议使用 计算属性 或 方法代替。❌
  • <transition-group> 不再默认渲染根元素,但仍可以使用 tag prop创建一个根元素。
  • template 支持在 v-for 时绑定 key
  • $attr 已包含 classstyle 属性。
  • 过渡 class 名修改。v-enter 改为 v-enter-fromv-leave 改为 v-leave-from。在<transition> 组件 leave-class 改为 leave-from-classleaveFromClassenter-class 改为 enter-from-classenterFromClass
  • 在 vue 2.x 中 v-forv-if 优先。vue 3.x 版本中 相反。
  • v-modal prop 和 事件默认名称 分别修改为 modalValueupdate:modalValue.sync 修饰符和 组件的 modal 选项移除,随之代替的是 :title.sync="xxx" 等价于 v-modal:title="xxx"

生命周期

  • destroyed 生命周期选项被重命名为 unmounted
  • beforeDestroy 生命周期选项被重命名为 beforeUnmount
Vue2Vue3
beforeCreatesetup
createdsetup
beforeMountonBeforeMount
mountedonMounted
beforeUpdateonBeforeUpdate
updatedonUpdated
beforeDestoryonBeforeUnmount
destoryedonUnmounted

注意setup 在 vue3 版本代替了 beforeCreatecreated,可以理解为 setup 执行在它们之间。

同时 setup 支持两个参数,分别为 propscontext。注意事项,代码中有注释。而且 setup 内部是没有 this,打印出来是 undefined。因此不能像 Vue 2.x 那样,通过 this.data 这种写法,所以创建了新的API,下文有说到。

setup 支持 return 一个对象,它可供模板直接使用,例如 return { count }<div></div>

vue
// .vue文件代码片段
<script>
import {
  onBeforeMount,
  onMounted,
  onBeforeUpdate,
  onUpdated,
  onBeforeUnmount,
  onUnmounted
} from "vue";
export default {
  name: "Life",
  props: {
    title: String,
  },
  /**
   * @description:
   * @param {object} props 不能使用ES6解构,否则会破坏响应式。可以使用 toRef() 进行解构
   * @param {object} context 非响应式,可使用ES6解构,包含attr、emit、slots
   * @return {*}
   */
  setup(props, context) {
    // 注意:在 setup() 内部,this 不会是该活跃实例的引用,因为 setup() 是在解析其它组件选项之前被调用的,所以 setup() 内部的 this 的行为与其它选项中的 this 完全不同。这在和其它选项式 API 一起使用 setup() 时可能会导致混淆。
    onBeforeMount(() => {
      console.log(props);
      console.log(context);
      console.log("传入的title是" + props.title);
      console.log("挂载前执行");
    });

    onMounted(() => {
      console.log("挂载后执行");
    });

    onBeforeUpdate(() => {
      console.log("更新前执行");
    });

    onUpdated(() => {
      console.log("更新后执行");
    });

    onBeforeUnmount(() => {
      console.log("卸载前执行");
    });

    onUnmounted(() => {
      console.log("卸载后执行");
    });
	
    return {}
  }
}
</script>

获取标签元素

在 Vue2 中,我们获取元素都是通过给元素一个 ref 属性,然后通过 this.$refs.xx 来访问的,但这在 Vue3 中已经不再适用了。

Vue 3 版本获取元素标签,有点类似 React 的感觉。

vue
<template>
  <div>
    <div ref="el">div元素</div>
  </div>
</template>

<script>
import { ref, onMounted } from 'vue'
export default {
  setup() {
    // 创建一个DOM引用,名称必须与元素的ref属性名相同
    const el = ref(null)

    // 在挂载后才能通过 el 获取到目标元素
    onMounted(() => {
      el.value.innerHTML = '内容被修改'
    })

    // 把创建的引用 return 出去
    return {el}
  }
}
</script>

注意

在 Vue 2.x 版本,可以使用 this.$children 直接访问当前实例的子组件。在 Vue 3版本中移除,建议使用 $refs

Vue 2.x中,在 v-for 中使用 ref 是返回数组形式的 $refs,而在 Vue 3 中不再是创建一个 $refs 数组,而是需要自己 从单个 ref 绑定获取多个 ref

vue
<template>
  <div v-for="item for list" :key="item" :ref="setItemsRef"></div>
</template>
<script>
export default {
  data() {
    return {
      itemRefs: []
    }
  },
  methods: {
    setItemsRef(el) {
      el && (this.itemRefs.push(el))
    }
  },
  beforeUpdate() {
    this.itemRefs = []
  }
}
</script>

自定义指令

在 vue 3中主要改变的是自定义指令的钩子函数(为了与生命周期统一)。

Vue2Vue3解析
bindbeforeMount指令绑定到元素后发生。只发生一次。
insertedmounted元素插入父 DOM 后发生
/beforeUpdate新的!这是在元素本身更新之前调用的
update移除当元素更新,但子元素尚未更新时,将调用此钩子
componentUpdatedupdated一旦组件和子级被更新,就会调用这个钩子
/beforeUnmount新的!它将在卸载元素之前调用。
unbindunmounted一旦指令被移除,就会调用这个钩子。也只调用一次
vue
<template>
  <input v-focus />
</template>

<script>
export default {
  directives: {
    focus: {
      mounted(el) {
        el.focus();
      }
    }
  }
}
</script>

Data

data 组件选项声明不再接收纯 object,而需要 function 声明。

(为了根实例与vue模板声明同步)

javascript
// Object 声明,Vue3中不适用
const app = new Vue({
	data: {
		a: 1
	}
})

// 而是全部改为 function 声明
const app = new Vue({
	data() {
        return {
            a: 1
        }
    }
})

注意

在 Vue 3 中 Mixin 合并的时候,data 不再是深层合并,而是浅层合并。

对之前版本项目深层合并影响有点大。

javascript
const Mixin = {
    data() {
        return {
            a: {
                b: 1,
                c: 2
            }
        }
    }
}

const Comp = {
    mixins: [Mixin],
    data() {
        return {
            a: {
                b: 2
            }
        }
    }
}

// 在vue 2.x中,合并后的data
{
    a: {
        b: 2,
        c: 2
    }
}

// 在vue 3中,合并后的data
{
    a: {
        b: 2
    }
}

🆕emits

Vue3 新增一个emits选项,类似组件中的 props 选项,用来定义组件可以向其父对象发出的事件

支持数组形式,也支持一个对象形式,定义传入事件参数的验证器

简单来说就是声明哪些事件可以给父组件,强烈建议使用

vue
<template>
  <div>
    <p>{{ text }}</p>
    <button v-on:click="$emit('accepted')">OK</button>
  </div>
</template>
<script>
  export default {
    props: ['text'],
    emits: ['accepted']
  }
</script>

注意

❗❗ Vue 3 中已移除 .native 修饰符,一切未在 emits 中声明的事件监听器都算入 $attr,且默认绑定到 组件根节点上。

vue
<!-- app.vue -->
<template>
  <my-button @click="handleTap" />
</template>
<script>
export default{
  methods: {
    handleTap(e) {
      // 会输出两次,一次是原生事件监听器监听到,一次是 $emit 触发
      console.log(e)
    }
  }
}
</script>

<!-- MyButton.vue -->
<template>
  <div>
  	<button @click="$emit('click', $event)">我的按钮</button>    
  </div>
</template>

模板

在 Vue 2.x 中 是不支持多个根节点的,往往需要我们在外层使用 div 包裹。

🆕**在 Vue 3支持多个根节点了,同时需要我们显式定义 $attr 分布 **。

vue
<!-- vue 2.x -->
<template>
  <div>
    <header>...</header>
    <main>...</main>
    <footer>...</footer>
  </div>
</template>

<!-- vue 3 支持-->
<template>
  <header>...</header>
  <main v-bind="$attr">...</main>
  <footer>...</footer>
</template>

创建一个实例⁉️

在 Vue 2.x 版本只支持全局导入,进而很多时候一些没用到的模块也进行导入,导致项目体积增大。 而在 Vue 3.x 版本支持按需导入。

javascript
// 3.x
import { createApp } from 'vue'
// 允许分开,也允许链式
createApp(App).mount('#app')

// 2.x
import Vue from 'vue'
new Vue({
  el: '#app',
  render: h => h(App),
  store,
  router
})

在Vue 2.x 版本中,当我们要使用全局API 的时候,往往我们是这么做👇

javascript
Vue.mixin(...)
Vue.directive(...)

// 但这样如果有两个根实例话,则会互相污染
const app1 = new Vue({ el: '#app-1' })
const app2 = new Vue({ el: '#app-2' })

而在 Vue3 版本中,任何全局改变 Vue 的行为都移动到实例上。

javascript
import { createApp } from 'vue'
const app = createApp(App).mount('#app')
app.mixin(...)
app.directive(...)
2.x 全局 API3.x 实例 API (app)
Vue.configapp.config
Vue.config.productionTipremoved
Vue.config.ignoredElementsapp.config.isCustomElement
Vue.componentapp.component
Vue.directiveapp.directive
Vue.mixinapp.mixin
Vue.useapp.use
Vue.prototypeapp.config.globalProperties

TIP

vue3中,如果想要在多个应用中共享配置,可以使用工厂函数来创建实例。

javascript
const createApp = options => {
    const app = createApp(options)
    app.directive(...)
    return app
}

Vue.prototype 替换为 config.globalProperties

javascript
// 在 vue 2.x 中,往往我们这样使用来达到可以在每个组件访问某个 property
Vue.prototype.$xxx = xxx

// 在 vue 3.x 版本中修改为
const app = createApp({})
app.config.globalProperties.$xxx = xxx

渲染函数

在 2.x 中, render 函数将自动接收 h 函数 (它是 createElement 的常规别名) 作为参数:

javascript
// Vue 2 渲染函数示例
export default {
  render(h) {
    return h('div')
  }
}

在 3.x 中,h 现在是全局导入的,而不是作为参数自动传递。

javascript
// Vue 3 渲染函数示例
import { h } from 'vue'

export default {
  render() {
    return h('div')
  }
}

挂载方式改变

javascript
// in vue 2.x
new Vue({
  template: <div>这是一个模板</div>
}).$mount('#app')

// 模板代替了 挂载DOM
// 结果会渲染为
<div>这是一个模板</div>

// in vue 3.x
Vue.createApp({
  template: <div>这是一个模板</div>
}).$mount('#app')

// 可以理解为在挂载的 DOM 上使用了 innerHTML
// 结果会渲染为
<div id="app">
 <div>这是一个模板</div>    
</div>

模板语法🈚

模板语法与2.x一致

附:2.6.0版本新增动态参数

vue
<a v-bind:[attributeName]="url"> ... </a>
// attributeName 会被动态求值,若值为null或一切非字符串数据最终都会报警告
// 建议使用计算属性, 避免使用大写字符来命名键名

新增API⁉️

reactive

返回对象的响应式副本,它是深层的转换。

一般用于引用类型object、array。

(因为proxy转换只会对当前的对象其作用,而嵌套的对象时不会发生响应式的,则需要遍历。因此这个 API也是这个原理,遍历对象中的所有属性) 详细可以查看一下我之前写的proxy实现响应式原理

提醒:Vue 2.x 版本使用的是Object.defineProperty 来实现响应式

javascript
import { reactive } from 'vue'
const obj = {
	a: 1,
	b: {
		c: 2,
        d: {
          e: 3,
        }
    }
}
const state = reactive(obj)
console.log(state)
console.log(state.b)
console.log(state.d)
// 结果如下

Vue3 快速上手

readonly

获取一个对象 (响应式或纯对象) 或 ref 并返回原始 proxy 的只读 proxy。它也是深层的,但都只是只读。

vue
<template>
  <div>
    <div>可读写{{ state.a }}</div>
    <div>只读{{ readState.a }}</div>
    <!-- 当你点击 修改可读写读 按钮,两个div都会变化,当你点击 修改只读,控制台会报一个警告 Set operation on key "a" failed: target is readonly. -->
    <button @click="add">修改可读写读</button>
    <button @click="add2">修改只读</button>
  </div>
</template>

<script>
import { readonly, reactive } from "vue";
export default {
  setup() {
    const state = reactive({ a: 1 });

    const readState = readonly(state);

    function add() {
      state.a++;
    }

    function add2() {
      readState.a++;
    }

    return {
      state,
      readState,
      add,
      add2
    };
  },
};
</script>

isProxy

检查对象是否是由 reactivereadonly 创建的 proxy。

isReactive

检查对象是否是 reactive 创建的响应式 proxy。

注意

如果 proxyreadonly 创建的,并且 readonly 是由普通对象创建的,则返回 false,若 readonly 是由响应式 proxy 创建的,则为 true

javascript
const state = reactive({ a: 1 })
const demo1 = readonly({ a: 2 })
console.log(isReactive(state)) // true
console.log(isReactive(demo1)) // false
console.log(isReactive(readonly(state))) // true

isReadonly

检查对象是否是由 readonly 创建的只读 proxy。

toRaw

返回 reactivereadonly 的原始对象。即改为非响应式,谨慎使用

javascript
const foo = {}
const reactiveFoo = reactive(foo)

console.log(toRaw(reactiveFoo) === foo)

markRaw

标记一个对象,使其永远不会转换为 proxy。返回对象本身。即退出响应式,深层的。

javascript
const foo = markRaw({})
console.log(isReactive(reactive(foo))) // false

shallowReactive

创建一个响应式 proxy,跟踪其自身 property 的响应性,但不执行嵌套对象的深度响应式转换 。简单来说就是浅层。就是我一开始说的 reactive 需要遍历,则它就不遍历。

vue
<template>
  <div>
    <p>响应式a:{{ state2.a }}</p>
    <p>非响应式b:{{ state2.b.c }}</p>
    <p>
      首先点击修改非响应式,点击后视图并没有发生修改,只有点击修改响应式后视图才监听到修改
    </p>
    <button @click="add">修改响应式</button>
    <button @click="add2">修改非响应式</button>
  </div>
</template>

<script>
import { reactive, shallowReactive, toRaw } from "vue";
export default {
  setup() {
    const obj = {
      a: 1,
      b: {
        c: 2,
        d: {
          e: 3
        }
      }
    }

    const state2 = shallowReactive(obj); // shallowReactive 接受一个参数,使之成为响应式对象,但是不执行深度响应式转换,即嵌套的对象不做proxy。其实了解proxy就明白reactive 和 shallowReactive的区别
    console.log(state2);
    console.log(state2.b);
    console.log(state2.b.d);

    function add() {
      state2.a++;
    }

    function add2() {
      state2.b.c++;
      // 当触发这个的时候,值已经改变了,但 shallowReactive 只会把自身属性改为响应式,而嵌套属性是非响应式的。
      console.log(state2);
    }

    return {
      state2,
      add,
      add2,
    };
  },
};
</script>

打印如下:

Vue3 快速上手

shallowReadonly

创建一个 proxy,使其自身的 property 为只读,但不执行嵌套对象的深度只读转换。和上面同理,都是浅层的

ref

接受一个内部值并返回一个响应式且可变的 ref 对象。ref 对象具有指向内部值的单个 property .value简单来说就是用来包装一个值成为响应式

注意

setup 内部使用的时候,需要显示加上 .value。但在模板中不需要。

可以理解为 ref(any) => reactive({value: any})

一般用于基本类型string、number、boolean。

vue
<!--
 * @Author: kim
 * @Date: 2020-11-30 15:08:04
 * @LastEditors: kim
 * @LastEditTime: 2020-11-30 16:51:21
 * @Description: 
-->
<template>
  <div>
    <p>{{ count }}</p>
    <button @click="add">点击</button>
  </div>
</template>

<script>
import {
  ref,
  reactive,
} from "vue";
export default {
  setup(props, context) {
    // ref 和 reactive 都是用来包装数据为一个响应式
    const count = ref(0); // 每次访问的时候需要加上 .value,但在模板中不需要。一般用于基本类型string、number、boolean。

    const add = () => {
      count.value++;
    }

    return { count, add };
  },
};
</script>

toRef

可以用来为源响应式对象上的 property 新创建一个 ref。然后可以将 ref 传递出去,从而保持对其源 property 的响应式连接。

js
const state = reactive({
  foo: 1,
  bar: 2
})

const fooRef = toRef(state, 'foo')

fooRef.value++
console.log(state.foo) // 2

state.foo++
console.log(fooRef.value) // 3

为什么会有这个API呢?

setup 中有提到,props 不能使用 ES6 进行解构,因为会破坏其响应式。不妨碍我们想一下解构的原理,解构是复制出来,即破坏了引用关系。

这个 API 就是用来代替解构,并且保持响应式的。

toRefs

将响应式对象转换为普通对象,其中结果对象的每个 property 都是指向原始对象相应 property 的ref。很好理解,toRef 对单属性,toRefs 对整个对象。简单来说就是原本是对象可以响应式的,后来对象不是响应式,但其下所有属性都是响应式,同时返回的对象支持 ES6 解构。

我们回想一下 reactive 打印出来是怎么样的👇

Vue3 快速上手

通过 toRefs 转换后👇

Vue3 快速上手

javascript
const obj = {
      a: 1,
      b: {
        c: 2,
        d: {
          e: 3,
        },
      },
    }

const state = reactive(obj)
const state2 = toRefs(state)
console.log(state2);

shallowRef

shallowReactive 是监听对象第一层的数据变化用于驱动视图更新,那么 shallowRef 则是监听 .value 的值的变化来更新视图的

vue
<template>
 <p>{{ state.a }}</p>
 <p>{{ state.first.b }}</p>
 <p>{{ state.first.second.c }}</p>
 <!-- 点击改变2,你会发生先数据变了,但视图没有改变,只有点击改变1,视图才发生变化 -->
 <button @click="change1">改变1</button>
 <button @click="change2">改变2</button>
</template>

<script>
import {shallowRef} from 'vue'
export default {
    setup() {
        const obj = {
          a: 1,
          first: {
            b: 2,
            second: {
              c: 3
            }
          }
        }
        
        const state = shallowRef(obj)
        console.log(state);
  
        function change1() {
          // 直接将state.value重新赋值
          state.value = {
            a: 7,
            first: {
              b: 8,
              second: {
                c: 9
              }
            }
          }
        }

        function change2() {
          state.value.first.b = 8
          state.value.first.second.c = 9
          console.log(state);
        }

        return {state, change1, change2}
    }
}
</script>

triggerRef

手动执行与 shallowRef关联的任何效果。简单来说就是手动刷新。

如上面那个例子中,我们在change2 加入一句代码 triggerRef() 视图也会发生改变。

provide&inject

与 Vue 2.x 中的 provideinject 作用相同。只不过 Vue 3 支持了组合式API,所以有了新的写法。

provide&inject 支持在组件中传递数据。众所周知父组件传递到子组件是使用 props,但如果嵌套这很深的组件,我们一层一层传递下去则非常烦人。

因此provide&inject 允许我们在父组件提供数据,所有子组件都可以获取到这个数据,不管层次有多深,且无需一层层传递下去。

但不要过度使用,否则会很难追踪到数据来源。当数据量多的时候建议使用 vuex 代替

vue
<!-- 在vue 2.x中 -->
<!-- 在父组件或祖先组件中注入 -->
<script>
export default {
  provide: {
    location: 'North Pole',
    geolocation: {
      longitude: 90,
      latitude: 135
    }
  }
}
</script>

<!-- 在子组件或子孙组件中使用 -->
<script>
export default {
  inject: ['location', 'geolocation']
}
</script>

Vue3 中允许我们显示导入 provide方法和 inject 方法。

  • provide方法,接受两个参数
  • name 键名
  • value
  • inject 方法,也接受两个参数
  • name 键名(provide设置的)
  • defaultValue 默认值(可选)
vue
<!-- 比如现在有三个组件 Father.vue、Son.vue、Grandson.vue -->
<!-- Father.vue -->
<template>
  <div>
    <h1>我是Father</h1>
    <Son/>
  </div>
</template>

<script>
import { provide } from "vue";
import Son from "./Son.vue";

export default {
  setup() {
    provide("title", "hello world");

    return {}
  },
  components: {
    Son
  }
};
</script>

<!-- Son.vue -->
<template>
  <div>
    <h2>我是Son</h2>
    <Grandson />
  </div>
</template>

<script>
import Grandson from "./Grandson.vue";

export default {
  components: {
    Grandson
  }
};
</script>

<!-- Grandson.vue -->
<template>
  <div>
    <h2>我是Grandson</h2>
    <div>我是来自祖父的值---{{ title }}</div>
  </div>
</template>

<script>
import { inject } from "vue";

export default {
  setup() {
    const title = inject("title", "这是默认值");

    return { title };
  },
};
</script>

效果如下👇

Vue3 快速上手

watch&watchEffect

watchwatchEffect 都是用来监视某项数据变化从而执行指定的操作的,但用法上还是有所区别。

watchwatch( source, cb, [options] )

参数说明

  • source:可以是表达式或函数,用于指定监听的依赖对象
  • cb:依赖对象变化后执行的回调函数
  • options:可参数,可以配置的属性有 immediate(立即触发回调函数)、deep(深度监听)

Tip

watch API 与 this.$watchwatch 选项是同一样东西,只是新语法而已。

默认 watch 方法默认是浅层的监听我们指定的数据,即如果监听的数据是嵌套多层,则深层的数据改变并不会触发监听回调,如果想要监听到,则需要传入第三个参数 deep

  • watchEffect 比较,watch 允许我们:
    • 惰性地执行副作用;
    • 更具体地说明应触发侦听器重新运行的状态;
    • 访问侦听状态的先前值和当前值。

watch方法会返回一个stop方法,若想要停止监听,便可直接执行该stop函数

javascript
// 当监听 ref 类型时
setup() { 
	const state = ref(0)
    watch(state, (newValue, oldValue) => {
		console.log(`原值为${oldValue}`)
        console.log(`新值为${newValue}`)
    })
}

// 监听 reactive 类型时
setup() { 
	const state = reactive({cout: 0})
    watch(() => state.count, (newValue, oldValue) => {
		console.log(`原值为${oldValue}`)
        console.log(`新值为${newValue}`)
    })
}

// 监听多个值的时候
setup() { 
	const state = reactive({cout: 0, a: 2})
    watch([() => state.count, () => state.a], ([newCount, newA], [oldCount, oldA]) => {
		console.log(oldCount)
        console.log(newCount)
        console.log(oldA)
        console.log(newA)
    })
}

watchEffect,它与 watch 的区别主要有以下几点:

  1. 不需要手动传入依赖
  2. 每次初始化时会执行一次回调函数来自动获取依赖
  3. 无法获取到原值,只能得到变化后的值

简单来说它会自动搜索监听回调中的依赖项,只要依赖项发生改变则会自动触发

vue
<script>
import {reactive, watchEffect} from 'vue'
export default {
    setup() { 
		const state = reactive({ count: 0 })

		watchEffect(() => {
            // 比如在这里它会自动监听state.count 
			console.log(state.count)
		})

        setTimeout(() => {
            state.count ++
        }, 1000)
    }
}
</script>

getCurrentInstance

因为 setup 函数中 this 的指向是 undefined,但我们在 Vue 2.x中经常使用的 this ,代表当前组件实例没有了吗?

getCurrentInstance 就是返回当前组件实例的。但不建议使用。

javascript
const instance = getCurrentInstance()
console.log(instance)

BUG

当你在测试环境下,console.log 打印 const { ctx } = getCurrentInstance(),大概是长这样的👇

Vue3 快速上手

然而当你打包后,在正式环境下打印,却长这样👇

Vue3 快速上手

然后接下来就是把 ctx 当成 this 使用的代码一顿报错。

临时解决方案

使用 const { proxy } = getCurrentInstance() 来代替 ctx ,但还是建议不要使用 getCurrentInstance

useStore

在 Vue2 中使用 Vuex,有时候通过 this.$store 来与获取到 Vuex 实例,但上面也说了原本 Vue2 中的 this 的获取方式不一样了,并且在 Vue3 的 getCurrentInstance().ctx 中也没有发现 $store 这个属性,那么如何获取到 Vuex 实例呢?这就要通过 vuex 中的一个方法了,即 useStore

vue
// store 文件夹下的 index.js
import Vuex from 'vuex'

const store = Vuex.createStore({
    state: {
      name: '至安'
    },
    mutations: {
      ……
    },
    ……
})

// example.vue
<script>
// 从 vuex 中导入 useStore 方法
import {useStore} from 'vuex'
export default {
    setup() { 
        // 获取 vuex 实例
        const store = useStore()

        console.log(store)
    }
}
</script>

Released under the MIT License.