首先,new Vue这个问题是针对vue2的。因为,在 Vue 2.x 版本中,需要使用
new Vue()
来创建 Vue 实例。这是 Vue 2.x 的传统用法,通过实例化 Vue 构造函数来创建根 Vue 实例。Vue 3.x 引入了 Composition API,并且推荐使用createApp()
来创建应用实例,而不是直接使用new Vue()。
new Vue贯穿了整个vue的生命周期,在挂载el前有模板渲染,在挂载后更新有虚拟DOM,diff算法,依赖收集等。整个过程是个非常全面的过程,每个细节点都很多。vue源码是个庞大又复杂的工程,本文在讲解new Vue的过程时,旨在纵向挖掘,讲清过程逻辑,不做横向展开。
new Vue是什么?
new Vue()
是 Vue.js 框架中用于创建根 Vue 实例的语法。当我们使用 Vue.js 构建一个单页面应用时,通常会先创建一个根 Vue 实例,然后在该实例中定义应用的数据、方法、计算属性等,以及挂载根组件。具体来说,new Vue()
的作用是:
- 创建一个 Vue 实例,该实例代表整个 Vue 应用的根实例。
- 通过传入的选项对象来配置该 Vue 实例,包括数据、计算属性、方法、生命周期钩子函数等。
- 将该 Vue 实例挂载到一个真实的 DOM 元素上,使得应用的内容可以显示在页面上。
根实例
在 Vue 应用中,通常会有一个根 Vue 实例,该实例会负责管理整个应用的状态和数据。这个根实例一般会挂载到一个 HTML 元素上,作为整个应用的容器。在 Vue 中,可以通过
el
属性指定挂载的目标元素或$mount
方法来手动挂载。这两种挂载方式会对应vue生命周期图的两个分支。
根实例:new Vue写在哪里?
new Vue()
创建 Vue 实例的过程是发生在main.js
中的。通常情况下,main.js
是整个应用的入口文件,负责初始化应用所需的各种配置和依赖。在main.js
中创建 Vue 实例,并指定挂载的目标元素,是非常常见的做法。通过在main.js
中创建 Vue 实例,可以确保整个应用的 Vue 实例都能正确挂载到指定的元素上,实现统一的管理和控制。同时,main.js
中创建的 Vue 实例也可以方便地进行状态管理、路由控制等操作。
根实例使用el挂载到#app
通过
new Vue()
创建了一个根 Vue 实例,并将其挂载到 id 为app
的 DOM 元素上,并且指定了该实例的data
选项
<!--index.html-->
<!DOCTYPE html>
<html lang="">
<body>
<div id="app">{{ message }}</div>
<!-- built files will be auto injected -->
</body>
</html>
//main.js
var app = new Vue({
el: '#app',
data: {
message: 'Hello Vue!'
}
})
el是什么?
el是一个属性,接收一个CSS 选择器,也可以是一个 HTMLElement 实例,el不能是body或者html。使用
el
属性来指定 Vue 实例挂载的目标元素,Vue 实例会自动将其挂载到该目标元素上
对比生命流程图分析el挂载过程
对照new Vue过程图,分析这个挂载执行流程:
在创建 Vue 实例时,通过
el
属性指定一个选择器字符串或一个 DOM 元素作为挂载目标。Vue 实例会自动查找该选择器对应的 DOM 元素,或直接使用传入的 DOM 元素作为挂载目标。如果在创建 Vue 实例时没有提供render
函数,Vue 会根据实例选项中的template
或挂载元素的 HTML 内容来编译模板。Vue 会将模板编译成渲染函数,用于将数据动态渲染到挂载的目标元素上。
根实例通过$mount挂载到#app
main.js
中创建一个 Vue 实例,并将App.vue
组件渲染到 id 为app
的 DOM 元素上,而这个 DOM 元素正是index.html
中提供的。因此,最终 Vue 应用会被挂载到index.html
中 id 为app
的<div>
元素上,用户将在浏览器中看到 Vue 应用渲染的内容。
App.vue 中的 #app: 在 App.vue
文件中,通常会包含应用的根组件,这个根组件会作为整个应用的入口。为了将根组件挂载到 index.html
中的 #app
元素上,通常会在 App.vue
文件中定义一个与 index.html
中的 #app
相对应的容器,以便将根组件挂载到正确的位置。
<!--index.html-->
<!DOCTYPE html>
<html lang="">
<body>
<div id="app"></div>
<!-- built files will be auto injected -->
</body>
</html>
<!--app.vue-->
<template>
<div id="app">
<img alt="Vue logo" src="./assets/logo.png">
<HelloWorld msg="Welcome to Your Vue.js App"/>
</div>
</template>
//main.js
import Vue from 'vue'
import App from './App.vue'
Vue.config.productionTip = false
new Vue({
render: h => h(App),
}).$mount('#app')
对比生命周期流程图分析$mount挂载过程
使用$mount挂载,
$mount
方法接收一个选择器字符串或一个 DOM 元素作为参数,用于指定 Vue 实例挂载的目标元素。调用$mount
方法后,Vue 实例会将自身挂载到指定的 DOM 元素上,从而开始管理该 DOM 元素及其子元素的状态和数据。如果在创建 Vue 实例时没有提供render
函数,Vue 会根据实例选项中的template
或挂载元素的 HTML 内容来编译模板。当调用$mount
方法时,Vue 会对模板进行编译,生成渲染函数,用于将数据渲染到 DOM 中
组件实例
为什么要区分根实例和vue实例呢?
因为很多人对这个new Vue很懵逼。在业务代码里,拢共就写了一个new Vue。但是你在写.vue页面的时候是不是每个组件都有一套生命周期钩子函数可以用,涉及挂载、更新、卸载等?可是我们没有显示的使用new Vue()创建组件实例啊?
在官方给的解释中说道:所有的vue组件都是Vue实例!
组件实例和根实例的区别
根实例是整个Vue应用的入口点,负责管理整个应用;而组件实例是构成应用的独立模块,每个组件实例之间相互独立,有自己的生命周期钩子函数和状态。在Vue中,所有的组件都是Vue实例,但是根实例和组件实例在功能和作用上有所区别。这个区别主要体现在以下几个方面:
-
根实例 vs 组件实例:
- 根实例是整个Vue应用的最顶层实例,负责管理整个应用的数据和行为。
- 组件实例是Vue应用中的组件,每个组件都是一个独立的Vue实例,拥有自己的数据、方法和生命周期钩子函数。
-
创建方式:
- 根实例通常是在
main.js
中通过new Vue()
创建的。 - 组件实例是在组件定义中通过
components
属性注册或者在单文件组件中直接定义的。
- 根实例通常是在
-
生命周期钩子函数:
- 根实例拥有完整的生命周期钩子函数,包括
beforeCreate
、created
、beforeMount
、mounted
、beforeUpdate
、updated
、beforeDestroy
、destroyed
。 - 组件实例也有自己的生命周期钩子函数,如
beforeCreate
、created
、beforeMount
、mounted
、beforeUpdate
、updated
、beforeDestroy
、destroyed
,用于控制组件的初始化、更新和销毁等过程。
- 根实例拥有完整的生命周期钩子函数,包括
-
组件复用和嵌套:
- 组件实例可以被复用和嵌套,每个组件实例之间相互独立,有自己的作用域和状态。
- 根实例是整个应用的顶级实例,负责管理所有组件实例,并提供全局配置和状态管理。
组件实例的new Vue过程怎么理解 ?
在Vue中,组件实例的创建并不需要显式地使用
new Vue()
,而是通过Vue组件系统来创建组件实例。当我们在Vue应用中定义一个组件时,Vue会在内部帮我们处理组件实例的创建和管理。在Vue中,组件实例的创建过程大致可以分为以下几个步骤:
-
组件定义: 我们在Vue应用中定义一个组件,可以是全局注册的组件,也可以是局部注册的组件(在父组件中注册)。
-
组件实例化: 当组件在模板中被使用时,Vue会根据组件定义自动实例化组件。这个过程是由Vue的编译器负责的。
-
虚拟DOM的创建: Vue会根据组件的模板(template)生成虚拟DOM(Virtual DOM)。
-
挂载组件: Vue会将生成的虚拟DOM挂载到实际的DOM元素上,这个过程是在组件的生命周期钩子函数中的
mounted
钩子中完成的。 -
更新组件: 当组件的数据发生变化时,Vue会重新渲染组件,并更新视图。这个过程是在组件的生命周期钩子函数中的
updated
钩子中完成的。 -
销毁组件: 当组件不再需要时(比如组件被销毁或者不再显示),Vue会在适当的时机销毁组件实例,释放组件占用的资源。这个过程是在组件的生命周期钩子函数中的
beforeDestroy
和destroyed
钩子中完成的。
总的来说,组件实例的创建过程是由Vue自动管理的,我们只需要定义好组件,然后在需要的地方使用组件即可。Vue会负责处理组件实例的创建、更新和销毁等过程,我们不需要手动去实例化组件。这种抽象的方式让我们可以更专注于组件的功能和交互,而不必过多关注底层的实例化细节。
new Vue生命周期过程
在了解了new Vue是干什么的,以及组件实例和根实例的作用,再来看vue组件实例/根实例的生命周期就有思路了。Vue 实例的生命周期过程可以分为以下几个阶段:
初始化阶段(Initialization):
beforeCreate
:在实例初始化之后,数据观测和事件配置之前被调用。created
:实例已经创建完成,数据观测和事件配置已完成,但尚未挂载到 DOM 上。模板编译阶段(Template Compilation):
beforeMount
:在挂载开始之前被调用:相关的 render 函数首次被调用。mounted
:实例已经挂载到 DOM 上后被调用,这时可以访问到DOM元素。更新阶段(Updating):
beforeUpdate
:数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁之前。updated
:数据更新后调用,发生在虚拟 DOM 重新渲染和打补丁之后。销毁阶段(Destruction):
beforeDestroy
:实例销毁之前调用。在这一步,实例仍然完全可用。destroyed
:实例销毁后调用。在这一步,Vue 实例的所有指令已经解绑,所有事件监听器已经移除,所有子实例也已经被销毁。
在 Vue 实例的生命周期中,开发者可以利用这些生命周期钩子函数来执行各种操作,比如在数据初始化前后执行某些逻辑、在 DOM 渲染前后执行某些操作、在组件销毁前后清理资源等。生命周期钩子函数提供了丰富的扩展能力,可以帮助开发者更好地控制应用的行为。
源码相关术语解释
源码中的类、构造函数、属性、方法、实例的解释
Vue:表示vue的构造函数
Vue.prototype.**:表示给构造函数增加**方法
vm:表示vue实例
vm.**:获取vm实例的属性或调用vm实例的方法
vm.$options:获取vm实例的$options属性
vnode:虚拟节点
vm._render:渲染函数,将模板转换虚拟dom
vm._update:调用组件更新方法
Watcher类:监听模板变化的监听器;监听vm数据变化重新渲染。数据变化执行updated
hook:生命周期名字
callHook(vm, '***') :在当前vue组件实例中,调用某个生命周期钩子注册的所有回调函数。
Vue构造函数和vm实例
vue实例,在源码中通过new function Vue构造函数的方式生成。源码通过mixin混入,将外部定义的Vue.prototype.**原型方法与Vue构造函数结合,扩展Vue的方法。
Vue原型总共提供了下面方法:事件相关$on\$once\$off\$emit;初始化相关_init;更新相关_update\$forceUpdate\$destroy;渲染相关$nextTick\_render;状态相关$set\$delete等。挂载$mount方法
vm可以访问哪些属性和方法呢 ?
在vue的类型定义,component.ts文件里可以看到vm的类型
vm.$options——获取vm实例相关属性
vm.$options:在源码中大量出现!用于 访问和操作 Vue 实例的初始化选项,在运行时方便地获取到实例的配置信息,并根据这些信息进行相应的处理。
可以通过$options获取vm实例的哪些属性呢?
从$options的类型定义ComponentOptions可以看到,$options对象可以访问的属性几乎包括了vm实例所有的事件、属性、方法。包括选项式api提供的data\props\computed\method\watch等;dom相关的挂载点、render函数等;生命周期相关的钩子函数;指令,依赖注入以及一些vm的私有属性。
源码解析
初始化事件绑定和父子关系
源码关于实例化过程,在 vue-main\src\core\instance\index.ts中,可以看到Vue的构造函数,function Vue()。同时,下面还做了混入方法,混入方法就是给Vue原型增加方法。
在vue-main\src\core\instance\init.ts文件中可以看到额外扩展了Vue原型的__init方法
initLifecycle方法
initLifecycle
方法主要完成了初始化了父子组件关系:
- 将当前组件的父组件设置为传入的
parent
组件。- 将当前组件添加到父组件的子组件列表中。
初始化组件的生命周期相关属性:
初始化了
_isMounted
属性,用来表示组件是否已经挂载。初始化了_isDestroyed
属性,用来表示组件是否已经销毁。初始化了_isBeingDestroyed
属性,用来表示组件是否正在被销毁。初始化了_inactive
属性,用来表示组件是否处于非活动状态。
initEvents方法
initEvents
主要负责初始化事件相关的数据结构,并处理父组件传递的事件监听器,确保组件在初始化时能够正确处理事件。initEvents
函数主要完成了以下几个步骤:
- 创建了一个空对象
_events
用来存储事件监听器。 - 将
_hasHookEvent
标记设置为 false,用来表示当前组件是否有钩子函数相关的事件。 - 检查是否有父组件传递的事件监听器,如果有的话则调用
updateComponentListeners
函数来更新组件的监听器。
初始化依赖注入和响应式数据
initInjections方法
initInjections
函数主要负责解析注入属性的值,为这些注入属性设置响应式,并在开发环境下给出警告,防止直接修改注入属性导致不可预料的问题
initState方法
initState
函数主要负责初始化组件的 props、setup、methods、data、computed 和 watch 等状态,为组件的各种状态做准备工作,以便在组件实例化时能够正确地使用这些状态。
在beforeCreate钩子的时候我们没有对
props
、methods
、data
、computed
、watch
上的数据的访问权限。在created
中才可以
initProvide方法
initProvide
函数的作用是初始化组件的 provide 数据,将 provide 数据提供给组件的后代组件使用。这样可以在组件树中实现跨层级传递数据,为子组件提供共享的数据或方法。
模板渲染
在created方法后,beforeMount之前,执行的是模板转换为render函数的过程。new Vue示例图中没有el属性才会去调$mount方法。但是在源码中,created钩子之后,判断el存在也会去调$mount方法。
$mount方法
$mount中进行模板渲染, 在源码中重写$mount方法。如果vue的$options上有render方法,则返回。否则,根据template生成render函数。没有template的话,调el挂载的html解析为template模板。不用进行render函数生成。
组件挂载
组件挂载的过程核心部分在mountComponent方法中实现。
mountCompontent方法
每个组件有对应的Watcher,数值变化会触发其update函数导致重新渲染mountComponent核心就是实例化一个渲染watcher,在它的回调函数中调用updateComponent方法。
updateComponent更新模板方法,其中有两个核心方法:vm_render用于生成虚拟Dom,和vm_.update将虚拟dom映射到真实dom。
在mountComponent方法里,使用vm.$el接收el变量。执行beforeMount钩子函数。设置更新模板逻辑updateComponent方法。设置watcher监听选项,内置了beforeUpdate方法。
核心是通过new Watcher创建了一个watcher实例,watcher监听组件vm实例,当数据发生变化时触发模板的更新。
在执行完
vm._update()
把VNode patch
到真实Dom后,执行mouted
钩子。也就明白了为什么直到mounted
阶段才名正言顺的拿到了Dom
组件更新
组件更新,核心调用的是_update方法
watcher.run() => componentUpdate() => render() => update() => patch()
这个方法里通过vm的__patch__方法比较新旧vnode节点差异,并将返回值复制给vm.$el。
看下__patch__在浏览器运行时,是patch方法。
patch方法又通过createPatchFunction创建。
createPatchFunction
方法
createPatchFunction
方法是Vue源码中的一个核心函数,主要负责创建一个用于对比和更新虚拟DOM树的patch
函数。在Vue中,虚拟DOM的概念被用来描述真实DOM树的轻量级表示,通过对比新旧虚拟DOM树的差异,可以高效地更新真实DOM,实现页面的响应式更新。
具体来说,createPatchFunction
方法主要完成以下几个功能:
- 根据传入的
backend
参数生成对应的patch
函数,backend
包含了一系列操作DOM的方法,如创建元素、插入元素、更新元素等。 - 在生成
patch
函数时,会根据平台的不同(如浏览器、Weex等),选择不同的backend
,以适配不同的环境。 patch
函数的核心功能是对比新旧虚拟DOM树的差异,并将这些差异应用到真实DOM上,从而实现页面的更新。patch
函数会递归地遍历新旧虚拟DOM树的节点,找出差异并更新对应的真实DOM节点,以确保页面与最新的虚拟DOM树保持同步。
总的来说,
createPatchFunction
方法的作用是生成一个用于对比和更新虚拟DOM树的patch
函数,是Vue实现响应式更新的重要组成部分。
本文不做虚拟dom和patch过程的详细展开
生命周期钩子函数
在创建一个Vue实例时,Vue会在实例化过程中执行一系列的生命周期钩子函数,这些钩子函数允许开发者在不同的阶段插入自定义逻辑。
以下是在new Vue
的过程中组件生命周期钩子函数做的事情:
beforeCreate:在实例初始化之后,数据观测 (data observer) 和 event/watcher 事件配置之前被调用。在这个阶段,实例的属性和方法是不可用的,在这个阶段,可以进行一些初始化工作,例如设置一些默认值、初始化一些变量或者进行一些全局配置。。
beforeCreate() {
console.log('组件即将创建');
this.message = 'Hello, Vue!';
}
created:在实例创建完成后被立即调用。在这个阶段,实例已经完成数据观测,属性和方法的运算,watch/event事件回调。但是挂载阶段还未开始,$el属性目前不可见。在这个阶段,可以发送网络请求获取数据,初始化一些异步数据,或者订阅一些事件。
created() {
axios.get('https://api.example.com/data')
.then(response => {
this.data = response.data;
})
.catch(error => {
console.error('Error fetching data: ', error);
});
}
beforeMount:在挂载开始之前被调用:相关的 render 函数首次被调用。
mounted:在实例被挂载之后调用,这时候 el 被新创建的 vm.el替换了。如果根实例挂载到了一个文档内的元素,当mounted被调用时vm.el 也在文档内。在这个阶段,可以执行一些需要等到DOM渲染完成后才能进行的操作,比如初始化第三方插件、绑定事件监听等。
mounted() {
this.$nextTick(() => {
// 初始化第三方插件
new ThirdPartyPlugin(this.$refs.myElement);
// 绑定事件监听
this.$refs.myElement.addEventListener('click', this.handleClick);
});
}
beforeUpdate:数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁之前。可以在该钩子中进一步地更改状态,不会触发附加的重渲染过程。
updated:由于数据更改导致的虚拟 DOM 重新渲染和打补丁的过程结束后调用。当这个钩子被调用时,组件DOM已经更新,所以你现在可以执行依赖于DOM的操作。
beforeDestroy:在实例销毁之前调用。实例仍然完全可用。
destroyed:在实例销毁之后调用。Vue实例会清理它与其它实例的连接,解绑它的全部指令及事件监听器。在这个阶段,可以进行一些清理工作,比如取消订阅事件、清除定时器、释放资源等。
beforeDestroy() {
// 取消订阅事件
EventBus.$off('eventName', this.eventHandler);
// 清除定时器
clearInterval(this.timer);
}
这些生命周期钩子函数允许开发者在不同的阶段插入自定义逻辑,以满足特定的需求,比如在数据加载前后进行一些操作,或者在组件销毁前进行清理工作等。通过合理利用这些生命周期钩子函数,可以更好地控制组件的行为和实现更复杂的交互逻辑。