前段时间用vue3搭建完成了一个小项目,可喜的是领导在设计之初同意使用vue3进行新项目的开发,在此从技术角度记录一下vue3与vue2不同的点。
在main.js中的全局挂载方式
vue2:$mount
import Vue from "vue";
import App from "./App.vue";
import store from "./store/";
import router from "./router";
new Vue({
router,
store,
render: h => h(App)
}).$mount("#app");
vue3:createApp:返回一个提供应用上下文的应用实例,应用实例挂载的整个组件树共享同一个上下文
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
const app = createApp(App)
app.use(router)
app.use(store)
app.mount('#app')
createApp方法扩展
- component:注册或检索全局组件
// 注册一个名为my-component的组件
app.component('my-component', {
/* ... */
})
// 检索注册的组件(始终返回构造函数)
const MyComponent = app.component('my-component')
-
config:包含应用配置的对象。以下是config的可配置项:
globalProperties:添加一个可以再应用的任何组件实例中访问的全局property
相当于vue2中的:Vue.prototype.$aa = '....'
在vue3中的使用:app.config.globalProperties.$aa = '...'
关于globalProperties的用法在下文有扩展~ -
directive:注册或检索全局指令
-
mixin:将一个mixin应用在整个应用范围内。
-
mount:所提供 DOM 元素的 innerHTML 将被替换为应用根组件的模板渲染结果。
-
provide:设置一个可以被注入到应用范围内所有组件中的值
-
unmount:卸载用用实例的根组件
-
use:安装vue.js插件。如果插件是一个对象,它必须报漏一个 install 方法;若它本身是一个函数,将被视为安装方法
-
version:以字符串形式提供已安装的vue的版本号
组合式API-setup
vue2中,我们书写逻辑部分,需要这样写:
export default {
components: {...},
props: {...},
data () {
return {
num: ''
}
},
computed: { },
watch: {},
mounted () {},
methods: {}
}
当组件开始变大时,逻辑关注点的列表也会增长,后期不便于维护。
vue3使用setup:
- setup在组件创建之前执行(即在beforeCreated钩子之前)
- setup中不再使用this, 因为它不会找到组件实例
- setup接收两个参数:props & context的函数, context包含三个参数{attrs / slots / emit}
export default {
components: {...},
props: {...},
setup(props, {attrs, slots, emit}) {
console.log(props)
//在这里可以写生命周期钩子函数
onMounted(() => {....})
// 在return返回所有用于DOM的变量和方法
return {}
}
}
结构简单了许多,写起来特别香~
生命周期钩子
如图是vue2和vue3的生命周期钩子对比:
vue3中给钩子函数都加上了“on”来访问
在单页面的引入
import {onMounted, onUpdated...} from 'vue'
这些函数接收一个回调函数,使用方法如上面例子
注意:钩子函数在使用之前必须要在单页面引入
响应式引用 ref & 响应式状态reactive
vue2中,双向绑定是基于Object.defineProperty()方法实现,且用this可以指向当前实例,所以一些简单的变量赋值就很容易的渲染到DOM上,完成响应。
vue3中,双向绑定是通过ES6的proxy方法实现,且页面不再使用this,响应式变量的定义就发生了变化:
ref
通过 ref 函数为变量创建了一个响应式引用。 使任何响应式变量在任何地方起作用
import { ref } from 'vue'
const counter = ref(0) // 括号里面是给counter赋初始值
因绑定方式发生变化,若打印counter 是如下结果:
所以,如果要对counter进行操作,需要用 counter.value。但是在DOM中双向绑定的时候不用.value,因为它会自动浅层次解包内部值,直接绑定就行。
ref一般用来定义基本数据类型的变量(Number,String,Boolean,Null, Undefined)
reactive
官方文档上对reactive的解释是:返回对象的响应式副本,为 JavaScript 对象创建响应式状态;该响应式转换是“深度转换”——它会影响传递对象的所有嵌套 property。
所以,reactive一般用来定义引用数据类型的变量(Object,Array)
const obj = reactive({ count: 0 })
打印obj的结果如下:
操作count值: obj.count
以下关于解包:
- reactive 将解包所有深层的 refs,同时维持 ref 的响应性
ex1:
const count = ref(1)
const obj = reactive({ count })
// ref 会被解包
console.log(obj.count === count.value) // true
count.value++
console.log(count.value) // 2
console.log(obj.count) // 2
ex2:当将 ref 分配给 reactive property 时,ref 将被自动解包。
const count = ref(1)
const obj = reactive({})
obj.count = count
console.log(obj.count) // 1
console.log(obj.count === count.value) // true
- reactive定义变量可以使用 for…of 进行循环
响应式状态解构
当定义了一个响应式对象,我们想要采用ES6解构的方式获取其中的参数进行操作时:
import { reactive } from 'vue'
const obj = reactive({
name: 'wang',
age: 18,
sex: '女',
height: 160
})
// 解构
const { name, age } = obj
console.log('name: ', name, name.value)
console.log('age: ', age, age.value)
结果如上,解构出的两个property失去响应性。
所以,vue3里引入了api: toRefs,保留与源对象的响应式关联
import { reactive, toRefs } from 'vue'
const obj = reactive({
name: 'wang',
age: 18,
sex: '女',
height: 160
})
// 解构
const { name, age } = toRefs(obj)
console.log('name: ', name, name.value)
console.log('age: ', age, age.value)
结果如下:
我们做进一步的测试,修改解构之后的name值,是否会反应到源对象obj上呢?
name.value = '小王'
console.log('name: ', name.value, obj)
如上,修改name值之后,源对象obj的值会同步更新,完美~
一个小栗子
基于上面的生命周期和响应式引用,我们来看一个较完整的小栗子:
<template>
<div>
<p>{{ num }}</p>
<p v-for="item in arr" :key="item">{{ item }}</p>
<button @click="getData">点击事件</button>
</div>
</template>
<script>
import { ref, reactive, onMounted } from 'vue'
export default {
setup() {
// 响应式
const num = ref(1)
const arr = reactive(['red', 'yellow'])
// 不具备响应式,可用于中间逻辑操作,但无法在dom中使用
let testNum = 0
// methods:
const getData = () => {
console.log('getData')
}
// 生命周期
onMounted(() => {
getData()
})
// DOM中使用到的响应式引用和方法需要return
return {
num,
arr,
getData
}
}
}
</script>
计算属性和侦听器
computed
在vue2中,computed是这么使用的:
computed: {
variable() {
const path = this.$route.path
return path
}
}
计算属性内部存在缓存,当返回值发生变化才会触发
在vue3中,computed这样使用:
import { computed } from 'vue'
setup() {
const variable = computed(() => localStorage.getItem('userid'))
}
写法简化
注意,vue3中的承载计算属性的变量 variable 同样具备响应式引用,使用时:variable.value
watch
在vue2中, watch这样使用:
watch: {
// 监听变量
watcherVal(newValue,oldValue) {
// 处理逻辑
}
}
watch 需要侦听特定的数据源,并在单独的回调函数中执行副作用。默认情况下,它也是惰性的——即回调仅在侦听源发生变化时被调用。
vue3中,这么用:
侦听单一源:
import { watch } from 'vue'
setup(){
const count = ref(0)
watch(count, (newValue, oldValue) => {
/* ... */
})
}
同时侦听多个源,采用数组形式:
import { watch } from 'vue'
setup(){
const var1 = ref(0)
const var2 = ref(1)
watch([var1, var2], ([newVar1, newVar2], [oldVar1, oldVar2]) => {
/* ... */
})
}
ref获取DOM
在vue使用ref获取DOM之前,我们一般使用js的原生api,用ref获取就方便了许多。尤其是在画echarts图时
而ref不止是可以获取DOM,同时可以担任起父子组件的传参~
我们先看vue2中ref的使用:
<template>
// 子组件
<test-component ref="testComp" />
<div ref="divDom"></div>
</template>
// methods:
methods:{
someFunction() {
// this.$refs.testComp 即可得到子组件DOM, initFunc:子组件方法
this.$refs.testComp.initFunc(params)
// 获取div的DOM
console.log(this.$refs.divDom)
}
}
在vue3中,使用组合式api(setup)时,响应式引用和模板引用的概念是统一的,即上面提到的定义响应式引用的ref和DOM中的ref是一个概念,那么ref获取DOM如下使用:
<template>
<div ref="root">hello world</div>
</template>
<script>
import { ref, onMounted } from 'vue'
export default {
setup() {
const root = ref(null)
onMounted(() => {
// DOM 元素将在初始渲染后分配给 ref
console.log(root.value) // <div>hello world</div>
})
return {
root
}
}
}
</script>
如上,定义一个与ref同名的变量,return之后,就可以使用 root.value 来获取DOM。
同时,若ref定义在子组件上,可以调用子组件方法进行传参,甚至修改子组件的变量值:
补充上面例子:
<template>
<div ref="root">This is a root element</div>
// 子组件
<test-component ref="testComp" />
</template>
<script>
import { ref, onMounted } from 'vue'
export default {
setup() {
const root = ref(null)
const testComp = ref(null)
const someFunction = () => {
// 执行子组件的initFunc方法,并传参params
testComp.value.initFunc(params)
}
onMounted(() => {
// DOM 元素将在初始渲染后分配给 ref
console.log(root.value) // <div>This is a root element</div>
console.log(testComp.value)
someFunction()
})
return {
root,
testComp
}
}
}
</script>
globalProperties方法扩展
上文也简单提到了globalProperties,可以定义全局property,在DOM中的使用和vue2一样没有变化:
app.config.globalProperties.$aa = '...'
$aa // DOM中用
但是在setup-methods中,因为缺少this指向,vue3文档中提到一个新的api:getCurrentInstance
getCurrentInstance:支持访问内部组件实例
不能滥用!
博主当时遇到的情况是:全局引入并定义了echarts之后,在方法中使用echarts来绘图,发现无法拿到全局的echarts,然后查到了该api
使用如下:
import { getCurrentInstance } from 'vue'
export default {
setup() {
const internalInstance = getCurrentInstance()
internalInstance.appContext.config.globalProperties // 访问 globalProperties
}
}
getCurrentInstance 只能在setup或生命周期钩子中调用
我们可以看一下,在我的项目中,internalInstance.appContext.config.globalProperties的打印:
nextTick
nextTick: 等待DOM更新之后执行
vue2:
this.$nextTick(() => {
// do something
})
vue3:
setup() {
const someFunc= async () => {
await nextTick()
console.log('DOM is updated')
}
prop&emit
来到父子组件传值啦~
先回想下vue2中的prop&emit的使用:
// 子组件:
<template>
<div>
<p> {{ vari1 }} </p>
<button @click="handleClick">触发</button>
</div>
</template>
<script>
export default {
props: {
vari1: {
type: String,
default: 'hi'
}
},
data(){
return{}
},
methods: {
handleClick() {
this.$emit('handleClickFunc')
}
}
}
</script>
// 父组件
<template>
<div>
// 子组件
<test-component :vari1="variable" @handleClickFunc="handleClickFunc" />
</div>
</template>
<script>
export default {
data() {
return {
variable: 'hello'
}
},
methods: {
handleClickFunc() {
console.log('hello world')
}
}
}
</script>
如上,是不是非常熟悉~
那么,vue3有什么改变呢?
我们通过上文也知道了api setup的两个参数setup(props, {attrs, slots, emit }){}。
props 对象将仅包含显性声明的 prop,并且所有声明了的prop,不论父组件是否向其传递,都会出现在props对象。
emit作用没变,不过vue3提供了一个emits选项,emits可以用来定义一个组件可以向其父组件触发的事件。
栗子如下:
// 子组件:
<template>
<div>
<p> {{ vari1 }} </p>
<button @click="handleClick">触发</button>
</div>
</template>
<script>
export default {
props: {
vari1: {
type: String,
default: 'hi'
}
},
// 定义组件可触发的事件
emits: ['handleClickFunc'],
setup(props, { emit }) {
console.log(props.vari1)
const handleClick = () => {
emit('handleClickFunc')
}
return {
handleClick
}
}
}
</script>
// 父组件
<template>
<div>
// 子组件
<test-component :vari1="variable" @handleClickFunc="handleClickFunc" />
</div>
</template>
<script>
export default {
setup() {
const variable = 'hello'
const handleClickFunc = () => {
console.log('hello world')
}
return {
variable,
handleClickFunc
}
}
}
</script>
父组件中除了新api之外没有特殊变化,还是子组件里增加了变化props&emits
vuex
创建一个store
想想vue2是怎么创建store的呢?
import Vue from "vue";
import Vuex from "vuex";
Vue.use(Vuex);
export default new Vuex.Store({
state: {},
mutations: {},
actions: {}
});
如上,创建成功之后在main.js中进行挂载
那么,vue3呢?
vue3引入了一个新api:createStore
如下:
import { createStore } from 'vuex'
// 创建一个新的 store 实例
const store = createStore({
state () {},
mutations: {},
actions: {}
})
创建成功之后在main.js中将store实例作为插件安装(.use)
这样就创建成功了。
useStore()
vue3中,通过调用函数useStore来在setup中访问store,相当于vue2中使用的this.$store
调用方式如下:
import { useStore } from 'vuex' // 引入
export default {
setup () {
const store = useStore()
}
}
如上,获取到store之后,就可以访问State、Getter、Mutation和Action
为了访问state和getter,需要computed
引用来保留响应式:
要使用 mutation 和 action 时,只需要在 setup 钩子函数中调用 commit 和 dispatch 函数:
import { computed } from 'vue'
import { useStore } from 'vuex'
export default {
setup () {
const store = useStore()
return {
// 在 computed 函数中访问 state
count: computed(() => store.state.count),
// 在 computed 函数中访问 getter
double: computed(() => store.getters.double)
// 使用 mutation
increment: () => store.commit('increment'),
// 使用 action
asyncIncrement: () => store.dispatch('asyncIncrement')
}
}
}
题外话:
vue2中有些辅助函数:mapState、mapGetters、mapMutations、mapActions,但是vue3里面是没有这些辅助函数的。目前只有useStore函数。
router
this.$router & useRouter()
vue2中,我们通过this.$router
来访问路由器,同时作用于路由跳转
this.$router.push('page')
this.$router.push({name:'Page', params: { name:'xiaowang' }})
vue3中, setup 里面没有访问 this,使用useRouter函数来代替this.$router
import { useRouter } from 'vue-router'
export default {
setup() {
const router = useRouter()
const someFunction = () => {
router.push({ name: 'Page', params: { name: 'xiaowang' } })
}
return {
someFunction
}
}
}
this.$route & useRoute()
vue2中,我们通过this.$route
来访问当前的路由,并作用于接收从this.$router
跳转页面携带的参数
// 接上面this.$router代码
this.$route.params.name // 'xiaowang'
vue3中,采用useRoute函数代替this.$route
import { useRoute } from 'vue-router'
export default {
setup() {
const route = useRoute()
const userData = ref()
// 当参数更改时获取用户信息
watch(
() => route.params,
(newParams) => {
userData.value = newParams.name
}
)
}
}
小结: 在模板中我们仍然可以访问 $router 和 $route,在 setup 中就需要使用函数。所以不必在setup中返回 router 和 route
总结
用vue3完成了一个历时近两月的小项目,一开始写有很多不理解的地方,但是随着熟练加深,发现vue3写起来是真香~代码逻辑结构更加清晰了,而且新的响应式原理,比2版本强大很多。有幸看到最后的朋友们可以尝试一波。
目前对于vue2和vue3不同点的总结就先更新到这,以后如果再遇到会持续更新。
最后。vue3不支持IE浏览器! 在实际项目中请谨慎!