Code前端首页关于Code前端联系我们

Vue3初始化的完整流程是怎样的?相比Vue2有哪些关键变化?

terry 2小时前 阅读数 16 #Vue

最近翻前端技术社区,看到不少刚从Vue2转过来或者从零学前端框架的朋友,在聊Vue3初始化的时候总有点绕:“只知道用createApp,点进去源码又看不懂一大堆函数”“之前Vue2的new Vue里有data、methods这些,现在setup都放到createApp().mount()外面的根组件了?初始化逻辑是不是完全重写了?”

其实别慌,Vue3的初始化虽然换了底层架构(从Options API主导的类式编程转向Composition API+函数式编程的混合),但核心目的和Vue2是一样的——搭建应用的运行时环境、处理根组件配置、挂载DOM节点、建立响应式系统和虚拟DOM更新机制,今天就拆解一下从你敲下npm create vue@latest生成项目,到浏览器里看到第一个“Hello Vue 3 + Vite”这个过程的关键步骤,顺便对比Vue2,说说这些变化到底解决了什么问题。

第一步:搭架子——从命令行到项目目录

不管是Vue2还是Vue3,初始化的第一步都要搭个空项目,但工具和目录已经大不一样了。 之前Vue2常用的是Vue CLI,现在官方推荐的是Vite+Create Vue的组合,虽然也有人用Webpack+Vue Loader,但Vite的冷启动和热更新速度确实香太多。 你敲npm create vue@latest my-vue3-app的时候,其实就是触发Create Vue脚手架的工作流程:它会先下载并执行最新版的create-vue包,然后弹出一系列交互选项——要不要TypeScript、要不要JSX、要不要Pinia、要不要Vue Router、要不要ESLint+Prettier这些,选完之后,它会根据你的配置生成项目文件,比如src/main.js(或者.ts)入口文件、src/App.vue根组件、package.json依赖管理文件这些。 这里要注意的是,Create Vue默认不会生成很多Vue CLI时代的配置文件(比如vue.config.js),如果你需要自定义Webpack/Vite的配置,得自己新建vite.config.js,但Vite的配置比Webpack简单太多,文档也全。

第二步:启动入口——从new Vue到createApp

生成好项目,打开src/main.js,你会看到这么几行代码:

import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
createApp(App).mount('#app')

这就是Vue3的核心启动入口,对比Vue2的:

import Vue from 'vue'
import App from './App.vue'
new Vue({
  render: h => h(App)
}).$mount('#app')

是不是简洁了很多?但别以为只是少了个new和render函数,背后的变化才是关键。

第一个变化:全局配置与实例分离

在Vue2里,所有的全局配置(比如Vue.use()注册插件、Vue.component()注册全局组件、Vue.directive()注册全局指令、Vue.mixin()混入全局逻辑)都是直接挂载在Vue构造函数上的,这就导致一个问题——如果你在同一个页面里同时运行多个Vue2应用,它们会共享同一个全局状态和配置,很容易出现冲突,比如你在应用A里注册了一个全局组件叫MyButton,应用B里也想注册同名组件,直接报错;应用A里用了一个全局mixin,修改了created钩子,应用B的所有组件也会被影响。 到了Vue3,这些全局配置都被移到了应用实例(也就是createApp返回的那个对象) 上,你可以这么写:

// 创建应用实例1
const app1 = createApp(App1)
app1.component('MyButton', Button1)
app1.use(plugin1)
app1.mount('#app1')
// 创建应用实例2
const app2 = createApp(App2)
app2.component('MyButton', Button2)
app2.use(plugin2)
app2.mount('#app2')

两个应用完全独立,互不干扰,这个变化对开发多页面应用或者微前端项目来说太重要了。

第二个变化:根组件实例与应用实例分离

在Vue2里,new Vue返回的对象既是应用实例(可以配置全局属性),也是根组件实例(有$data$props这些实例属性,有生命周期钩子),这就导致根组件的职责很混乱,而且全局属性和根组件的属性也容易冲突。 到了Vue3,createApp返回的是纯粹的应用实例,只负责全局配置;根组件实例是在mount的时候才创建的,你可以通过应用实例的config.globalProperties来给所有组件(包括根组件)添加全局属性,

const app = createApp(App)
app.config.globalProperties.$api = 'https://api.example.com'
app.mount('#app')

然后在任意组件里(不管是Options API还是Composition API)都可以用this.$api或者getCurrentInstance().proxy.$api访问。

第三步:解析根组件——从合并选项到编译+解析

当你调用createApp(App)的时候,它其实已经做了一些基础工作,但真正的根组件解析是在调用mount()的时候开始的。

什么是根组件解析?

就是把你写的App.vue文件(不管是单文件组件还是JSX组件)转换成浏览器能识别的DOM结构,并且建立响应式系统。

Vue3解析根组件的流程

  1. 编译模板(如果是运行时+编译器版本) 首先要说明的是,Vue3有两个版本:
    • 运行时+编译器版本(vue.esm-browser.js/vue.esm-bundler.js):包含模板编译器,可以直接解析<template>标签里的内容或者字符串模板;
    • 仅运行时版本(vue.runtime.esm-browser.js/vue.runtime.esm-bundler.js):不包含模板编译器,只能解析预编译好的render函数。 现在的Vite和Webpack+Vue Loader默认都会使用仅运行时版本,因为它们会在打包的时候把<template>标签里的内容预编译成render函数,这样可以减少最终的打包体积(模板编译器大概占Vue3总大小的30%)。 如果是运行时+编译器版本,解析根组件的第一步就是调用compile函数,把模板字符串转换成render函数和staticRenderFns数组(静态节点的渲染函数)。
  2. 创建根组件实例 这一步会调用createComponentInstance函数,创建一个根组件的实例对象,里面包含了组件的所有信息:props、attrs、slots、setupContext、data(如果是Options API的话)、methods(如果是Options API的话)、computed(如果是Options API的话)、watch(如果是Options API的话)、生命周期钩子等等。 这里要注意的是,Composition API的setup函数是在组件实例创建之后、beforeCreate钩子之前执行的,这也是为什么setup函数里不能用this——因为此时组件实例的this还没有完全初始化。
  3. 执行setup函数(如果有) setup函数是Vue3的核心,它接收两个参数:props和setupContext(包含attrs、slots、emit、expose),然后返回一个对象(可以包含响应式数据、方法、computed、watch等,这些会暴露给模板和Options API的this)或者一个render函数(直接覆盖组件的模板)。 在执行setup函数之前,Vue3会先调用initProps函数,把父组件传递的props处理成响应式的(如果是shallow的话就是浅响应式),然后调用initSlots函数,把父组件传递的插槽处理成函数。 执行setup函数的时候,Vue3会调用createSetupContext函数创建setupContext,然后把props和setupContext传给setup函数,最后把setup函数的返回值挂载到组件实例的setupState上(如果返回的是对象的话)。
  4. 执行Options API的钩子和初始化(如果有) 如果你在根组件里同时用了Composition API和Options API,Vue3会先执行setup函数,然后再执行Options API的beforeCreate、created钩子,再初始化data、methods、computed、watch等。 这里要注意的是,Options API的data会被合并到组件实例的data上,然后通过proxy代理到setupState上,也就是说,你在setup函数里返回的响应式数据会覆盖Options API的data里的同名数据。
  5. 编译/解析完成,准备挂载 解析完根组件之后,Vue3会得到一个预编译好的render函数(不管是打包时预编译的还是运行时编译的),然后准备把它挂载到DOM节点上。

第四步:挂载DOM节点——从$mount到mount

挂载DOM节点是Vue3初始化的最后一步,也是最核心的一步,这一步会建立虚拟DOM、真实DOM和响应式系统之间的联系。

Vue3挂载DOM节点的流程

  1. 清空挂载目标的内容 Vue3会检查你传入的挂载目标(比如#app)是不是一个DOM元素,如果是字符串的话,会先调用document.querySelector转换成DOM元素;如果找不到的话,会报错(在开发模式下)。 Vue3会清空挂载目标的所有子节点,为后续的虚拟DOM渲染做准备。
  2. 创建虚拟DOM树(VNode树) 这一步会调用根组件的render函数,生成一个根VNode,VNode是Virtual Node的缩写,也就是虚拟DOM节点,它是一个JavaScript对象,包含了真实DOM节点的所有信息:标签名、属性、事件、子节点等等。 这里要注意的是,Vue3的VNode比Vue2的更轻量、更高效,因为它去掉了一些冗余的属性,并且用了更紧凑的数据结构。
  3. 把VNode树转换成真实DOM树 这一步会调用render函数,把根VNode转换成真实DOM树,并且插入到挂载目标里。 在转换的过程中,Vue3会做以下几件事:
    • 遍历VNode树,为每个VNode创建对应的真实DOM元素;
    • 把VNode的属性、事件绑定到真实DOM元素上;
    • 把VNode的子节点转换成真实DOM树,插入到父DOM元素里;
    • 建立VNode和真实DOM元素之间的映射关系(存储在VNode的el属性上);
    • 如果是组件VNode的话,会递归地创建组件实例、执行setup函数、解析组件、生成组件的VNode树、转换成真实DOM树。
  4. 触发组件的生命周期钩子 在挂载的过程中,Vue3会触发组件的一系列生命周期钩子:
    • beforeMount:在真实DOM插入到页面之前触发;
    • mounted:在真实DOM插入到页面之后触发。
  5. 挂载完成 挂载完成之后,浏览器里就能看到你的Vue3应用了,响应式系统也已经建立好了——当你修改响应式数据的时候,Vue3会自动更新虚拟DOM树,然后通过Diff算法对比新旧虚拟DOM树,找出差异,最后只更新差异部分的真实DOM,提高性能。

第五步:对比Vue2的挂载流程,看看优化了啥

很多朋友可能会说,Vue2的挂载流程不也是差不多吗?清空目标、生成VNode、转真实DOM、触发钩子,其实不然,Vue3在挂载流程上做了很多优化,其中最核心的就是Diff算法的优化静态节点的提升

Diff算法的优化

Vue2的Diff算法是全量对比新旧VNode树的所有节点,不管节点是不是静态的,而Vue3的Diff算法是靶向更新

  • Vue3会在编译模板的时候,给每个动态节点添加一个patchFlag(补丁标记),用来标记这个节点的哪些属性是动态的(比如文本、class、style、事件等);
  • 在Diff的时候,Vue3只会对比有patchFlag的节点,并且只会对比patchFlag标记的属性,这样可以大大减少Diff的时间;
  • Vue3还会把连续的静态节点合并成一个静态块,在Diff的时候直接跳过,进一步提高性能。

静态节点的提升

Vue2在每次调用render函数的时候,都会重新创建静态节点的VNode,这其实是没有必要的,因为静态节点的内容不会变化,而Vue3在编译模板的时候,会把静态节点的VNode提升到render函数外面,每次调用render函数的时候只会复用这些VNode,不会重新创建,这样可以减少内存的占用和渲染的时间。

总结一下Vue3初始化的完整流程

从命令行到浏览器看到页面,Vue3的初始化可以分为以下几个关键步骤:

  1. 搭架子:用Vite+Create Vue生成项目;
  2. 启动入口:调用createApp创建应用实例,传入根组件;
  3. 解析根组件:编译模板(如果需要)、创建根组件实例、执行setup函数、初始化Options API;
  4. 挂载DOM节点:清空目标、生成VNode树、转真实DOM树、触发钩子、建立响应式系统。

写在最后

可能有些朋友看完之后还是觉得有点复杂,其实没关系——作为普通开发者,你不需要完全看懂Vue3的源码,只要知道它的核心流程和关键变化,就能更好地使用Vue3开发项目。 如果你想深入学习Vue3的源码,可以先从createApprenderpatch这几个核心函数入手,慢慢啃,相信你会有很大的收获。 如果你在开发Vue3项目的时候遇到了问题,可以多翻官方文档,官方文档写得非常详细,而且有很多示例代码,比查第三方博客管用多了。

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

热门