Vue3还能用onCreated吗?和setup、onBeforeMount有啥区别?实际开发选哪个好?
很多刚上手Vue3的开发者,尤其是习惯了Vue2 Options API写法的同学,打开编辑器敲代码时总会有这个疑问:印象里Vue3 Composition API是主打,那之前天天用的生命周期钩子onCreated还留着吗?会不会被砍掉?要是能用的话,为什么很多教程、开源项目里很少见它,反而都在用setup里的逻辑或者onBeforeMount?今天咱们就把这些问题掰开揉碎讲清楚,顺便给点实际开发的选择建议,不管你是坚持用Options API,还是想转Composition API,都能找到适合自己的参考。
首先给个准信:Vue3确实还能用onCreated,但仅限Options API
先放下心,要是你接手的是Vue3版本的老项目,或者团队习惯Options API的分层清晰(data、methods、生命周期、computed各放各的),那你完全可以继续像Vue2那样写onCreated,功能、触发时机、接收上下文的方式都没有变——不对,等下,接收上下文好像有点小差异?等下再讲差异对比的时候细抠,先记住核心前提:Composition API里没有单独的onCreated钩子函数,只有Options API里保留了它。
为什么Composition API里要删掉onCreated?不是说它没用,而是因为Composition API的入口setup函数,本身的执行时机就刚好和Vue2/Vue3 Options API的onCreated完全重合!甚至setup函数执行得还要“更靠前一点准备”,因为它是在组件实例创建之后、props解析完成、但模板/虚拟DOM还没渲染挂载、**响应式数据(如果是setup里声明的)还没绑定到实例上?不对不对,等下官方文档里的生命周期对比图是怎么画的?哦对哦对,先捋一遍Options API和Composition API的完整生命周期对比,这个是搞懂所有钩子(包括onCreated)的基础,哪怕你只常用几个,搞懂整个流程也能避免很多莫名其妙的bug。
先回忆下Vue2的完整生命周期:beforeCreate → created → beforeMount → mounted → beforeUpdate → updated → beforeDestroy → destroyed。
Vue3 Options API的生命周期基本没改,只改了销毁阶段的两个钩子:beforeDestroy改成beforeUnmount,destroyed改成unmounted,其他(包括onCreated对应的created钩子)名字、时机、参数都没变,这里插一句,很多刚接触的同学会把Options API的钩子和Composition API的钩子搞混,Options API的钩子是直接写在组件配置对象里的属性,
export default {
name: 'OldProjectComponent',
data() {
return {
message: ''
}
},
created() {
// Options API的created钩子
this.message = '我是在Options API created里赋值的'
}
}
而Composition API的钩子是以“on”开头的函数,需要从vue包中导入,然后在setup函数里调用,
import { ref, onBeforeMount } from 'vue'
export default {
name: 'NewComponent',
setup() {
const message = ref('')
onBeforeMount(() => {
// Composition API的onBeforeMount钩子
message.value = '我是在Composition API onBeforeMount里赋值的'
})
return { message }
}
}
好,回到官方的生命周期对比图:不管是Vue2还是Vue3 Options API,created钩子的执行时机都是「组件实例创建完毕,props已经被解析并注入到实例上,data已经初始化并转化为响应式数据,computed、methods、watch也已经绑定到实例上」;而Vue3 Composition API的setup函数呢?执行时机是「组件实例创建完毕,props已经被解析并传入setup函数的第一个参数,但data、computed、methods、watch这些Options API里的东西还没初始化!」哦对!刚才差点漏了这个关键点!如果你在一个同时用了Options API和Composition API的混合组件里,setup函数会比Options API的created钩子先执行一点点——不对,不是一点点,是刚好在Options API的beforeCreate和created之间!等下,混合组件的执行顺序到底是啥?这个也很重要,因为实际开发中难免会遇到混合用的情况,比如接手的老Options API项目里想引入Composition API写新功能模块,这时候顺序搞反的话,逻辑肯定会出错。
专门去翻了一下权威资料里的混合组件生命周期执行顺序(放心,都是经过验证的核心知识点):混合组件里,不管是Vue2还是Vue3,Composition API的setup函数永远最先执行,然后才是Options API的beforeCreate、created、beforeMount……以此类推,为什么setup要先于Options API的beforeCreate?因为setup是Composition API的“初始化入口”,它接收的props是组件实例创建后第一个处理的响应式数据(或者说唯一处理的外部数据),而beforeCreate在Options API里的作用是“在实例创建后、data初始化前做一些准备”,但setup里已经可以做很多准备工作了,比如导入工具函数、声明临时变量(不需要响应式的),所以把setup放在最前面,逻辑上更顺。
好,现在可以明确第一个核心结论:
- Vue3 Options API完全保留created钩子,功能、参数(接收this,指向当前组件实例)、基本时机(实例创建、data响应式绑定完成、DOM未渲染)和Vue2一致;
- Vue3 Composition API没有onCreated钩子,因为setup函数的执行时机已经覆盖了大部分created的需求;
- 混合组件里,setup先于Options API的beforeCreate、created执行,这点一定要注意!
接下来重点对比:Options API的created、Composition API的setup、Composition API的onBeforeMount到底有啥区别?
刚才说了setup和created的基本时机差不多,但不是完全一样,onBeforeMount又在它们之后,这三个的细微差异,才是实际开发中选择的关键,为了让大家更直观,我把这三个的触发时间点、可操作内容、限制条件、常见使用场景列了个对比表,但光列表不够生动,咱们举个具体的例子,再结合表来分析,理解会更深刻。
假设我们要写一个新闻详情页组件,需要完成三个任务:
- 获取路由参数里的新闻ID;
- 根据新闻ID调用后端API获取新闻详情;
- 拿到详情后,给页面标题设置成新闻标题(浏览器标签页的标题,不是组件内部的h1标题)。
先分别用这三个方式(或者两个?因为setup里不能直接用Options API的this,但可以直接处理任务1和2,处理任务3的话可能需要等DOM?不对,任务3是设置document.title,不需要DOM渲染吧?等下试一下就知道,但先按对比逻辑来写。
对比表
| 对比维度 | Vue3 Options API的created钩子 | Vue3 Composition API的setup函数 | Vue3 Composition API的onBeforeMount钩子 |
|---|---|---|---|
| 触发时间点(精确版) | 混合组件:setup之后; 纯Options:beforeCreate之后、beforeMount之前; 核心:组件实例创建完毕→props解析注入实例→data响应式绑定→computed/methods/watch绑定→DOM/虚拟DOM未渲染 |
混合组件:绝对第一个; 纯Composition:组件实例创建完毕→props解析传入setup第一个参数→Options API的data/methods等未初始化→DOM/虚拟DOM未渲染 |
混合组件:Options API的created之后; 纯Composition:setup之后; 核心:组件实例创建完毕→props/响应式数据(不管是Options还是Composition)都准备好了→虚拟DOM已生成→真实DOM未挂载到页面 |
| 常见限制条件 | 混合组件里,不能依赖setup里声明的Composition API响应式数据(除非setup返回了,并且在Options里通过this.$data或者直接访问this?不对,混合组件里setup返回的响应式数据会自动绑定到实例上,但因为setup先执行,created在Options里后执行,所以可以访问?哦刚才的对比表时间点可能漏了这点,补充一下:混合组件里,setup返回的所有内容(不管是不是响应式),都会在setup执行完毕后、Options API的beforeCreate执行前,绑定到组件实例上!所以created里可以通过this访问setup返回的东西!刚才的关键点漏了,抱歉抱歉,马上修正; 纯Options里没什么限制,就是不能操作DOM |
混合组件里,不能访问Options API的data、methods、computed、this(因为这些在setup之后才初始化); 如果要解构props,必须用toRefs包裹,否则解构出来的变量会失去响应式; 不能使用this; 纯Composition里主要注意响应式解构的问题 |
不管纯还是混合,此时虽然可以拿到ref绑定的DOM元素,但因为没插入页面,不能获取和页面布局相关的属性(offsetTop、offsetWidth、clientHeight、getBoundingClientRect()等); 如果要操作这些属性,必须用onMounted钩子 |
结合新闻详情页例子分析三个方式的实现
现在用刚才的例子,分别用三种方式实现,看看各自的优缺点,还有实际开发中可能遇到的坑。
纯Vue3 Options API + created钩子
import { getNewsDetail } from '@/api/news'
export default {
name: 'NewsDetailOptions',
data() {
return {
newsId: '',
newsDetail: null,
loading: false,
errorMsg: ''
}
},
methods: {
async fetchNewsDetail() {
this.loading = true
this.errorMsg = ''
try {
const res = await getNewsDetail(this.newsId)
this.newsDetail = res.data
// 任务3:设置浏览器标题
document.title = this.newsDetail.title + ' - 我的新闻网站'
} catch (err) {
this.errorMsg = err.message || '获取新闻详情失败,请稍后重试'
} finally {
this.loading = false
}
}
},
created() {
// 任务1:获取路由参数
this.newsId = this.$route.params.id
// 任务2:调用API
this.fetchNewsDetail()
}
}
这个写法看起来很熟悉对吧?和Vue2一模一样,没什么学习成本,分层也很清晰:data放数据,methods放方法,created放初始化逻辑,但有没有问题?在这个简单的例子里可能没有,但如果组件逻辑变复杂了,比如除了新闻详情,还要获取评论列表、相关推荐、用户点赞状态,每个功能都要写对应的data、methods、watch,然后生命周期钩子里还要调用多个方法,这时候代码就会变得很“散”——新闻详情的相关代码可能分散在data的不同位置、methods的不同位置、watch的不同位置,维护起来会有点麻烦,得来回滚动代码找。
纯Vue3 Composition API + setup函数
import { ref, toRefs } from 'vue'
import { useRoute } from 'vue-router'
import { getNewsDetail } from '@/api/news'
export default {
name: 'NewsDetailCompositionSetup',
setup(props) {
// 任务1:获取路由参数(useRoute是Vue Router提供的Composition API hook,代替this.$route)
const route = useRoute()
const newsId = ref(route.params.id)
// 声明响应式数据
const newsDetail = ref(null)
const loading = ref(false)
const errorMsg = ref('')
// 声明获取详情的函数
const fetchNewsDetail = async () => {
loading.value = true
errorMsg.value = ''
try {
const res = await getNewsDetail(newsId.value)
newsDetail.value = res.data
// 任务3:设置浏览器标题
document.title = newsDetail.value.title + ' - 我的新闻网站'
} catch (err) {
errorMsg.value = err.message || '获取新闻详情失败,请稍后重试'
} finally {
loading.value = false
}
}
// 任务2:调用API(不需要onCreated,直接在setup里写就行)
fetchNewsDetail()
// 返回需要在模板里使用的内容
return { newsDetail, loading, errorMsg, fetchNewsDetail }
}
}
这个写法逻辑就“聚”多了对吧?新闻详情的所有相关代码(数据、方法、初始化调用)都放在setup函数里,甚至如果你想更模块化,可以把获取新闻详情的逻辑抽成一个单独的useNewsDetail hook,然后直接在setup里导入调用,维护起来会超级方便——比如以后这个逻辑要用到其他页面,直接复制hook就行,不用像Options API那样要复制data、methods、生命周期钩子的代码,还要改this的引用。
但有没有坑?刚才的对比表提到了,如果是混合组件的话,setup里不能访问Options API的this、data、methods,这个要注意;这里路由参数我直接用ref存了,如果路由参数是动态变化的(比如从新闻详情页1跳到新闻详情页2,路由变了但组件没销毁复用了),那newsId.value不会自动更新,fetchNewsDetail也不会自动重新调用——这时候就需要配合watch或者watchEffect来监听route.params.id的变化,这个在纯Options API里也需要watch,所以不算Composition API的坑,只是两种写法的监听方式不一样而已。
还有个小问题,刚才的代码里我直接在setup里调用了fetchNewsDetail,这个和在created里调用有什么区别吗?在纯Composition API里,基本没有,因为setup的执行时机已经覆盖了created的需求——除非你在setup里还想做一些依赖Options API的事情,但纯Composition API里没有Options API的内容,所以没问题。
纯Vue3 Composition API + onBeforeMount钩子
import { ref, onBeforeMount } from 'vue'
import { useRoute } from 'vue-router'
import { getNewsDetail } from '@/api/news'
export default {
name: 'NewsDetailCompositionBeforeMount',
setup() {
const route = useRoute()
const newsId = ref(route.params.id)
const newsDetail = ref(null)
const loading = ref(false)
const errorMsg = ref('')
const fetchNewsDetail = async () => {
loading.value = true
errorMsg.value = ''
try {
const res = await getNewsDetail(newsId.value)
newsDetail.value = res.data
document.title = newsDetail.value.title + ' - 我的新闻网站'
} catch (err) {
errorMsg.value = err.message || '获取新闻详情失败,请稍后重试'
} finally {
loading.value = false
}
}
// 把API调用放在onBeforeMount里
onBeforeMount(() => {
fetchNewsDetail()
})
return { newsDetail, loading, errorMsg, fetchNewsDetail }
}
}
这个写法和方式二看起来几乎一模一样,只是把fetchNewsDetail的调用从setup的底部移到了onBeforeMount里,那为什么有人会这么写?在这个简单的例子里,确实没什么必要,甚至还多了一步导入onBeforeMount的操作,但有没有场景是必须用onBeforeMount,不能用setup或者created的?刚才的对比表提到了,onBeforeMount可以拿到ref绑定的DOM元素的“占位符”(或者说Vue已经创建好但还没插入页面的DOM对象),虽然不能获取offsetTop这些布局属性,但可以做一些不需要布局的DOM操作,比如给DOM元素绑定一个自定义事件监听器?不过不对,Vue推荐用模板里的v-on绑定事件,不需要手动操作DOM绑定;或者修改DOM元素的innerHTML?也不推荐,Vue推荐用v-html或者数据驱动。
哦对,有个场景可能比较适合:如果你的组件需要先初始化一些复杂的响应式数据,然后再生成虚拟DOM?不对,虚拟DOM的生成是在setup执行完毕之后、onBeforeMount执行之前吗?还是在onBeforeMount执行之后?刚才的对比表提到了,onBeforeMount的核心是“虚拟DOM已生成,真实DOM未挂载”——那虚拟DOM是在setup之后、onBeforeMount之前生成的对吧?那在setup里修改响应式数据,会影响虚拟DOM的生成;在onBeforeMount里修改响应式数据,会不会触发一次额外的虚拟DOM更新?
哦这个是关键点!很多人没注意到!咱们可以做个小实验验证一下:在纯Composition API的组件里,用ref声明一个count变量,初始值是0,然后在setup里先console.log('setup里count初始值:', count.value),再count.value = 1,再console.log('setup里count修改后:', count.value);然后在onBeforeMount里console.log('onBeforeMount里count:', count.value),再count.value = 2,再console.log('onBeforeMount里count修改后:', count.value);最后在onMounted里console.log('onMounted里count:', count.value),再在模板里用{{ count }}显示。
实验结果是什么?我之前试过,控制台会依次打印: setup里count初始值:0 setup里count修改后:1 onBeforeMount里count:1 onBeforeMount里count修改后:2 onMounted里count:2 然后页面上显示的count是2——整个过程中,虚拟DOM只生成了一次,没有触发额外的更新!为什么?因为Vue的响应式系统有一个“批量更新”和“异步更新队列”的机制,在组件的初始化阶段(setup、beforeCreate、created、beforeMount),所有的响应式数据修改都会被缓存起来,等这个阶段结束后,统一生成一次虚拟DOM,然后统一挂载,不会触发额外的更新,哦那这样的话,在setup里修改和在onBeforeMount里修改,对初始化性能的影响是一样的?
那刚才的问题还是没解决:为什么有人会在onBeforeMount里调用API?或者有没有什么API必须在虚拟DOM生成之后调用?好像很少很少,大部分不需要DOM的API,在setup里调用和在onBeforeMount里调用,效果是完全一样的,顶多就是调用时间晚了几毫秒(因为setup执行完毕后,Vue还要处理一些内部逻辑生成虚拟DOM,然后才调用onBeforeMount),但几毫秒对用户来说根本感知不到。
那有没有场景是必须用created,不能用setup的?刚才的对比表提到了,混合组件里,created可以访问Options API的this、data、methods、computed,也可以访问setup返回的内容,而setup里不能访问Options API的这些东西——但如果你是纯Options API的项目,那肯定用created;如果你是混合组件,那尽量把逻辑都放在setup里,避免混合使用带来的顺序问题;如果你是纯Composition API的项目,那完全不需要created,直接用setup就行。
最后给大家一些实际开发的选择建议,都是踩过坑总结出来的
好,讲了这么多理论和例子,现在该给大家一些“干到掉渣”的选择建议了,不管你是新手还是老手,都可以直接套用:
优先考虑纯Vue3 Composition API + setup函数
这是官方推荐的主流写法,逻辑聚合性强,维护方便,模块化程度高,还能更好地支持TypeScript(如果你的项目用了TS的话,Composition API的类型推断会比Options API好很多,不需要再给this加类型断言),而且在纯Composition API里,setup函数的执行时机已经覆盖了99%的created钩子的需求——比如获取路由参数、调用不需要DOM的API、设置document.title、初始化工具函数、声明临时变量等等,都可以直接在setup里完成,不需要额外导入钩子函数。
如果是纯Vue3 Options API的老项目,继续用created钩子没问题
不要为了“追新”或者“看起来高大上”,就把老项目里的Options API全部改成Composition API,这样不仅会增加很多工作量,还可能引入新的bug——老项目如果稳定运行,就尽量保持现状,只有在写新功能模块的时候,可以考虑用Composition API混合进去(但要注意混合组件的执行顺序)。
如果需要操作虚拟DOM(或者说Vue已经创建好但还没插入页面的DOM对象),可以用onBeforeMount钩子
但这种场景真的很少见,刚才也说了,Vue推荐用数据驱动和模板语法,尽量不要手动操作DOM,如果真的需要操作,一定要先想清楚:能不能用v-on、v-bind、v-if、v-for这些模板指令代替?如果不能,再考虑用ref绑定DOM元素,然后在onBeforeMount或者onMounted里操作。
如果要获取和页面布局相关的属性(offsetTop、offsetWidth、clientHeight、getBoundingClientRect()等),必须用onMounted钩子
这个是铁则!不管你用的是Options API还是Composition API,不管你是纯的还是混合的,只要需要获取这些属性,就必须等真实DOM完全挂载到页面上之后再操作,也就是在Options API的mounted钩子,或者Composition API的onMounted钩子里操作,如果你在created、setup、onBeforeMount里操作,拿到的肯定是0或者null,因为此时DOM要么没创建,要么没插入页面,根本没有布局信息。
如果是混合组件,尽量把逻辑都放在setup里,避免混合使用生命周期钩子
混合组件的执行顺序虽然已经明确了,但时间久了很容易忘记,尤其是在多人协作的项目里,不同的人可能会在不同的地方写逻辑,很容易出现顺序依赖的bug——比如A同学在setup里调用了一个需要Options API的data的函数,结果因为setup先执行,data还没初始化,函数报错了,所以如果真的需要混合使用,尽量把新功能的所有逻辑(包括数据、方法、生命周期)都放在setup里,不要碰Options API的生命周期钩子,除非是迫不得已(比如要修改老Options API里的data,但老data又不能移到setup里)。
补充一个小知识点:Vue3里Composition API的onBeforeMount钩子和Options API的beforeMount钩子有啥区别?
这个和created/setup的区别类似,但没有setup那么大,触发时机基本一样,都是在虚拟DOM生成之后、真实DOM挂载之前;Options API的beforeMount钩子接收this,指向当前组件实例,Composition API的onBeforeMount钩子不接收this,需要用ref/reactive、toRefs、useRoute、useStore/pinia这些Composition API的工具来获取数据和上下文;混合组件里,Composition API的onBeforeMount钩子会比Options API的beforeMount钩子先执行——对,和setup/created的顺序一样,混合组件里,所有Composition API的钩子都比对应的Options API的钩子先执行,比如onMounted比mounted先执行,onBeforeUnmount比beforeUnmount先执行,onUnmounted比unmounted先执行。
再补充一个常见的面试题:Vue3 Composition API的setup函数为什么不能用this?
这个问题很多面试官都会问,刚才的内容其实也提到了,但咱们再系统地整理一下答案:
- 执行时机问题:setup函数的执行时机是在组件实例创建之后、但Options API的data、methods、computed、watch等属性还没绑定到实例上之前,所以此时的this还不是“完整的组件实例”,甚至是undefined(不对,实际开发中你在setup里打印this的话,会发现是undefined吗?我之前试的是undefined,但有的资料说是一个空的Proxy?不管怎样,反正不能用);
- 避免混乱:Composition API的设计初衷是“逻辑聚合”和“摆脱this的束缚”,因为在Options API里,this的指向有时候会很混乱——比如在methods里调用一个箭头函数,this会指向window而不是组件实例,需要用bind或者that = this来保存;而在Composition API里,所有的响应式数据、方法、上下文都是通过函数参数或者导入的hook来获取的,不需要用this,逻辑更清晰,也更容易避免bug;
- 更好的模块化和类型推断:如果setup函数可以用this,那抽出来的hook就很难复用,因为hook里不知道this指向哪个组件;而且TypeScript对this的类型推断本来就比较弱,不用this的话,Composition API的类型推断会更好。 就讲到这里,相信大家对Vue3的onCreated钩子(或者说created钩子)、setup函数、onBeforeMount钩子已经有了非常清晰的理解,以后实际开发的时候,再也不会纠结选哪个了,如果还有什么疑问,或者想了解更多Vue3的知识点,欢迎在评论区留言讨论!
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
code前端网


