Vue源码系列讲解——生命周期篇【二】(new Vue)
目录
1. 前言
2. new Vue()都干了什么
3 . 合并属性
4. callHook函数如何触发钩子函数
5. 总结
1. 前言
上篇文章中介绍了Vue实例的生命周期大致分为4个阶段,那么首先我们先从第一个阶段——初始化阶段开始入手分析。从生命周期流程图中我们可以看到,初始化阶段所做的工作也可大致分为两部分:第一部分是new Vue(),也就是创建一个Vue实例;第二部分是为创建好的Vue实例初始化一些事件、属性、响应式数据等。接下来我们就从源码角度来深入分析一下初始化阶段所做的工作及其内部原理。
2. new Vue()都干了什么
初始化阶段所做的第一件事就是new Vue()创建一个Vue实例,那么new Vue()的内部都干了什么呢? 我们知道,new 关键字在 JS中表示从一个类中实例化出一个对象来,由此可见, Vue 实际上是一个类。所以new Vue()实际上是执行了Vue类的构造函数,那么我们来看一下Vue类是如何定义的,Vue类的定义是在源码的src/core/instance/index.js 中,如下:
function Vue (options) {if (process.env.NODE_ENV !== 'production' &&!(this instanceof Vue)) {warn('Vue is a constructor and should be called with the `new` keyword')}this._init(options)
}
可以看到,Vue类的定义非常简单,其构造函数核心就一行代码:
this._init(options)
调用原型上的_init(options)方法并把用户所写的选项options传入。那这个_init方法是从哪来的呢?在Vue类定义的下面还有几行代码,其中之一就是:
initMixin(Vue)
这一行代码执行了initMixin函数,那initMixin函数又是从哪儿来的呢?该函数定义位于源码的src/core/instance/init.js 中,如下:
export function initMixin (Vue) {Vue.prototype._init = function (options) {const vm = thisvm.$options = mergeOptions(resolveConstructorOptions(vm.constructor),options || {},vm)vm._self = vminitLifecycle(vm)initEvents(vm)initRender(vm)callHook(vm, 'beforeCreate')initInjections(vm) // resolve injections before data/propsinitState(vm)initProvide(vm) // resolve provide after data/propscallHook(vm, 'created')if (vm.$options.el) {vm.$mount(vm.$options.el)}}
}
可以看到,在initMixin函数内部就只干了一件事,那就是给Vue类的原型上绑定_init方法,同时_init方法的定义也在该函数内部。现在我们知道了,new Vue()会执行Vue类的构造函数,构造函数内部会执行_init方法,所以new Vue()所干的事情其实就是_init方法所干的事情,那么我们着重来分析下_init方法都干了哪些事情。
首先,把Vue实例赋值给变量vm,并且把用户传递的options选项与当前构造函数的options属性及其父级构造函数的options属性进行合并(关于属性如何合并的问题下面会介绍),得到一个新的options选项赋值给$options属性,并将$options属性挂载到Vue实例上,如下:
vm.$options = mergeOptions(resolveConstructorOptions(vm.constructor),options || {},vm
)
接着,通过调用一些初始化函数来为Vue实例初始化一些属性,事件,响应式数据等,如下:
initLifecycle(vm)       // 初始化生命周期
initEvents(vm)        // 初始化事件
initRender(vm)         // 初始化渲染
callHook(vm, 'beforeCreate')  // 调用生命周期钩子函数
initInjections(vm)   //初始化injections
initState(vm)    // 初始化props,methods,data,computed,watch
initProvide(vm) // 初始化 provide
callHook(vm, 'created')  // 调用生命周期钩子函数
可以看到,除了调用初始化函数来进行相关数据的初始化之外,还在合适的时机调用了callHook函数来触发生命周期的钩子,关于callHook函数是如何触发生命周期的钩子会在下面介绍,我们先继续往下看:
if (vm.$options.el) {vm.$mount(vm.$options.el)
}
在所有的初始化工作都完成以后,最后,会判断用户是否传入了el选项,如果传入了则调用$mount函数进入模板编译与挂载阶段,如果没有传入el选项,则不进入下一个生命周期阶段,需要用户手动执行vm.$mount方法才进入下一个生命周期阶段。
以上就是new Vue()所做的所有事情,可以看到,整个初始化阶段都是在new Vue()里完成的,关于new Vue()里调用的一些初始化函数具体是如何进行初始化的,我们将在接下来的几篇文章里逐一介绍。下面我们先来看看上文中遗留的属性合并及callHook函数是如何触发生命周期的钩子的问题。
3 . 合并属性
在上文中,_init方法里首先会调用mergeOptions函数来进行属性合并,如下:
vm.$options = mergeOptions(resolveConstructorOptions(vm.constructor),options || {},vm
)
它实际上就是把 resolveConstructorOptions(vm.constructor) 的返回值和 options 做合并,resolveConstructorOptions 的实现先不考虑,可简单理解为返回 vm.constructor.options,相当于 Vue.options,那么这个 Vue.options又是什么呢,其实在 initGlobalAPI(Vue) 的时候定义了这个值,代码在 src/core/global-api/index.js 中:
export function initGlobalAPI (Vue: GlobalAPI) {// ...Vue.options = Object.create(null)ASSET_TYPES.forEach(type => {Vue.options[type + 's'] = Object.create(null)})extend(Vue.options.components, builtInComponents)// ...
}
首先通过 Vue.options = Object.create(null) 创建一个空对象,然后遍历 ASSET_TYPES,ASSET_TYPES 的定义在 src/shared/constants.js 中:
export const ASSET_TYPES = ['component','directive','filter'
]
所以上面遍历 ASSET_TYPES 后的代码相当于:
Vue.options.components = {}
Vue.options.directives = {}
Vue.options.filters = {}
最后通过 extend(Vue.options.components, builtInComponents) 把一些内置组件扩展到 Vue.options.components 上,Vue 的内置组件目前 有<keep-alive>、<transition> 和<transition-group> 组件,这也就是为什么我们在其它组件中使用这些组件不需要注册的原因。
那么回到 mergeOptions 这个函数,它的定义在 src/core/util/options.js 中:
/*** Merge two option objects into a new one.* Core utility used in both instantiation and inheritance.*/
export function mergeOptions (parent: Object,child: Object,vm?: Component
): Object {if (typeof child === 'function') {child = child.options}const extendsFrom = child.extendsif (extendsFrom) {parent = mergeOptions(parent, extendsFrom, vm)}if (child.mixins) {for (let i = 0, l = child.mixins.length; i < l; i++) {parent = mergeOptions(parent, child.mixins[i], vm)}}const options = {}let keyfor (key in parent) {mergeField(key)}for (key in child) {if (!hasOwn(parent, key)) {mergeField(key)}}function mergeField (key) {const strat = strats[key] || defaultStratoptions[key] = strat(parent[key], child[key], vm, key)}return options
}
可以看出,mergeOptions函数的 主要功能是把 parent 和 child 这两个对象根据一些合并策略,合并成一个新对象并返回。首先递归把 extends 和 mixins 合并到 parent 上,
 const extendsFrom = child.extendsif (extendsFrom) {parent = mergeOptions(parent, extendsFrom, vm)}if (child.mixins) {for (let i = 0, l = child.mixins.length; i < l; i++) {parent = mergeOptions(parent, child.mixins[i], vm)}}
然后创建一个空对象options,遍历 parent,把parent中的每一项通过调用 mergeField函数合并到空对象options里,
const options = {}
let key
for (key in parent) {mergeField(key)
}
接着再遍历 child,把存在于child里但又不在 parent中 的属性继续调用 mergeField函数合并到空对象options里,
for (key in child) {if (!hasOwn(parent, key)) {mergeField(key)}
}
最后,options就是最终合并后得到的结果,将其返回。
这里值得一提的是 mergeField 函数,它不是简单的把属性从一个对象里复制到另外一个对象里,而是根据被合并的不同的选项有着不同的合并策略。例如,对于data有data的合并策略,即该文件中的strats.data函数;对于watch有watch的合并策略,即该文件中的strats.watch函数等等。这就是设计模式中非常典型的策略模式。
关于这些合并策略都很简单,我们不一一展开介绍,仅介绍生命周期钩子函数的合并策略,因为我们后面会用到。生命周期钩子函数的合并策略如下:
/*** Hooks and props are merged as arrays.*/
function mergeHook (parentVal,childVal):  {return childVal? parentVal? parentVal.concat(childVal): Array.isArray(childVal)? childVal: [childVal]: parentVal
}LIFECYCLE_HOOKS.forEach(hook => {strats[hook] = mergeHook
})
这其中的 LIFECYCLE_HOOKS 的定义在 src/shared/constants.js 中:
export const LIFECYCLE_HOOKS = ['beforeCreate','created','beforeMount','mounted','beforeUpdate','updated','beforeDestroy','destroyed','activated','deactivated','errorCaptured'
]
这里定义了所有钩子函数名称,所以对于钩子函数的合并策略都是 mergeHook 函数。mergeHook 函数的实现用了一个多层嵌套的三元运算符,如果嵌套太深不好理解的话我们可以将其展开,如下:
function mergeHook (parentVal,childVal):  {if (childVal) {if (parentVal) {return parentVal.concat(childVal)} else {if (Array.isArray(childVal)) {return childVal} else {return [childVal]}}} else {return parentVal}
}
从展开后的代码中可以看到,它的合并策略是这样子的:如果 childVal不存在,就返回 parentVal;否则再判断是否存在 parentVal,如果存在就把 childVal 添加到 parentVal 后返回新数组;否则返回 childVal 的数组。所以回到 mergeOptions 函数,一旦 parent 和 child 都定义了相同的钩子函数,那么它们会把 2 个钩子函数合并成一个数组。
那么问题来了,为什么要把相同的钩子函数转换成数组呢?这是因为Vue允许用户使用Vue.mixin方法(关于该方法会在后面章节中介绍)向实例混入自定义行为,Vue的一些插件通常都是这么做的。所以当Vue.mixin和用户在实例化Vue时,如果设置了同一个钩子函数,那么在触发钩子函数时,就需要同时触发这个两个函数,所以转换成数组就是为了能在同一个生命周期钩子列表中保存多个钩子函数。
4. callHook函数如何触发钩子函数
关于callHook函数如何触发钩子函数的问题,我们只需看一下该函数的实现源码即可,该函数的源码位于src/core/instance/lifecycle.js 中,如下:
export function callHook (vm: Component, hook: string) {const handlers = vm.$options[hook]if (handlers) {for (let i = 0, j = handlers.length; i < j; i++) {try {handlers[i].call(vm)} catch (e) {handleError(e, vm, `${hook} hook`)}}}
}
可以看到,callHook函数逻辑非常简单。首先从实例的$options中获取到需要触发的钩子名称所对应的钩子函数数组handlers,我们说过,每个生命周期钩子名称都对应了一个钩子函数数组。然后遍历该数组,将数组中的每个钩子函数都执行一遍。
5. 总结
本篇文章介绍了生命周期第一个阶段——初始化阶段中所做的第一件事:new Vue()。
首先,分析了new Vue()时其内部都干了些什么。其主要逻辑就是:合并配置,调用一些初始化函数,触发生命周期钩子函数,调用$mount开启下一个阶段。
接着,就合并属性进行了详细介绍,知道了对于不同的选项有着不同的合并策略,并挑出钩子函数的合并策略进行了分析。
最后,分析了callHook函数的源码,知道了callHook函数如何触发钩子函数的。
接下来后面几篇文章将对调用的这些初始化函数进行逐个分析。
相关文章:
Vue源码系列讲解——生命周期篇【二】(new Vue)
目录 1. 前言 2. new Vue()都干了什么 3 . 合并属性 4. callHook函数如何触发钩子函数 5. 总结 1. 前言 上篇文章中介绍了Vue实例的生命周期大致分为4个阶段,那么首先我们先从第一个阶段——初始化阶段开始入手分析。从生命周期流程图中我们可以看到ÿ…...
JavaScript 设计模式之观察者模式
观察者模式 观察者模式又被称为发布-订阅模式,使用一个对象来收集订阅者,在发布时遍历所有订阅者,然后将信息传递给订阅者,可以这样来实现一个简单的模式 const Observable (function () {let __messages {}return {register:…...
 
数据结构D4作业
1.实现单向循环链表的功能 loop.c #include "loop.h" loop_p create_loop() { loop_p H(loop_p)malloc(sizeof(loop)); if(HNULL) { printf("创建失败\n"); return NULL; } H->len0; H->nextH; ret…...
 
springboot750人职匹配推荐系统
springboot750人职匹配推荐系统 获取源码——》公主号:计算机专业毕设大全...
 
【笔记】【开发方案】APN 配置参数 bitmask 数据转换(Android KaiOS)
一、参数说明 (一)APN配置结构对比 平台AndroidKaiOS文件类型xmljson结构每个<apn>标签是一条APN,包含完成的信息层级数组结构,使用JSON格式的数据。最外层是mcc,其次mnc,最后APN用数组形式配置&am…...
 
Redis篇之缓存雪崩、击穿、穿透详解
学习材料:https://xiaolincoding.com/redis/cluster/cache_problem.html 缓存雪崩 什么是缓存雪崩 在面对业务量较大的查询场景时,会把数据库中的数据缓存至redis中,避免大量的读写请求同时访问mysql客户端导致系统崩溃。这种情况下&#x…...
 
【深度学习笔记】3_2线性回归的从零实现
注:本文为《动手学深度学习》开源内容,仅为个人学习记录,无抄袭搬运意图 3.2 线性回归的从零开始实现 在了解了线性回归的背景知识之后,现在我们可以动手实现它了。尽管强大的深度学习框架可以减少大量重复性工作,但若…...
Apache Maven简介
Maven 简介 Apache Maven 是一个用于项目构建、依赖管理和项目信息管理的强大工具。它基于项目对象模型(Project Object Model,POM)进行构建,通过描述项目的结构和依赖关系来管理项目的构建过程。 以下是 Apache Maven 的一些关键原理和工作流程: 项目对象模型(POM)…...
#12解决request中getReader()和getInputStream()只能调用一次的问题
目录 1、背景 2、解决方案 2.1、自定义HttpServletRequestWrapper 2.2、JsonRequestHeaderParamsHelper 2.3、HttpServletRequestReplacedFilter 2.4、使用 1、背景 当前系统Content-Type为application/json,参数接收方式采用RequestBody和RequestParam&#…...
直接插入排序+希尔排序+冒泡排序+快速排序+选择排序+堆排序+归并排序+基于统计的排序
插入排序:直接插入排序、希尔排序 交换排序:冒泡排序、快速排序 选择排序:简单选择排序、堆排序 其他:归并排序、基于统计的排序 一、直接插入排序 #include<stdio.h> #include<stdlib.h> /* 直接插入排序&#…...
Java高级 / 架构师 场景方案 面试题(二)
1.双十一亿级用户日活统计如何用 Redis快速计算 在双十一这种亿级用户日活统计的场景中,使用Redis进行快速计算的关键在于利用Redis的数据结构和原子操作来高效地统计和计算数据。以下是一个基于Redis的日活统计方案: 选择合适的数据结构: …...
 
C/C++内存管理学习【new】
文章目录 一、C/C内存分布二、C语言中动态内存管理方式:malloc/calloc/realloc/free三、C内存管理方式3.1 new/delete操作内置类型3.2 new和delete操作自定义类型四、operator new与operator delete函数五、new和delete的实现原理5.1 内置类型 六、定位new表达式(pl…...
 
选择适合你的编程语言
引言 在当今瞬息万变的技术领域中,选择一门合适的编程语言对于个人职业发展和技术成长至关重要。每种语言都拥有独特的设计哲学、应用场景和市场需求,因此,在决定投入时间和精力去学习哪种编程语言时,我们需要综合分析多个因素&a…...
【力扣每日一题】力扣106从中序和后序遍历序列构造二叉树
题目来源 力扣106从中序和后序遍历序列构造二叉树 题目概述 给定两个整数数组 inorder 和 postorder ,其中 inorder 是二叉树的中序遍历, postorder 是同一棵树的后序遍历,请你构造并返回这颗 二叉树 。 思路分析 后序遍历序列的最末尾数…...
logback日志回滚原理
日志输出主要依赖RollingFileAppender、TimeBasedRollingPolicy、SizeAndTimeBasedFNATP。 RollingFileAppender 主要用于生成日志文件,格式化内容再输出到日志文件TimeBasedRollingPolicy 设置回滚策略,如果发现日志输出的时间超过单位时间,…...
 
[C#]winform基于opencvsharp结合pairlie算法实现低光图像增强黑暗图片变亮变清晰
【低光图像增强介绍】 在图像处理领域,低光图像增强是一个具有挑战性的任务。由于光线不足,这些图像往往呈现出低对比度、高噪声和细节丢失等问题,严重影响了图像的视觉效果和后续分析的准确性。因此,开发有效的低光图像增强方法…...
 
React18源码: reconcliler启动过程
Reconcliler启动过程 Reconcliler启动过程实际就是React的启动过程位于react-dom包,衔接reconciler运作流程中的输入步骤.在调用入口函数之前,reactElement(<App/>) 和 DOM对象 div#root 之间没有关联,用图片表示如下: 在启…...
 
【RN】为项目使用React Navigation中的navigator
简言 移动应用基本不会只由一个页面组成。管理多个页面的呈现、跳转的组件就是我们通常所说的导航器(navigator)。 React Navigation 提供了简单易用的跨平台导航方案,在 iOS 和 Android 上都可以进行翻页式、tab 选项卡式和抽屉式的导航布局…...
 
CS50x 2024 - Lecture 8 - HTML, CSS, JavaScript
00:00:00 - Introduction 关于互联网是怎么工作的,如何在他的基础上构建软件 HTML和CSS是描述性语言 javascript一种编程语言,在浏览器上下文中很有用,使得界面更具交互性,也用于服务器 00:01:01 - Bingo Board 00:01:51 - T…...
 
C++:派生类的生成过程(构造、析构)
目录 派生类的生成过程 派生类的构造函数与析构函数: 构造函数: 派生类组合类的构造和析构: 构造函数和析构函数调用顺序: 派生类的生成过程 三步骤: 吸收基类(父类)成员:实现代…...
 
23-Oracle 23 ai 区块链表(Blockchain Table)
小伙伴有没有在金融强合规的领域中遇见,必须要保持数据不可变,管理员都无法修改和留痕的要求。比如医疗的电子病历中,影像检查检验结果不可篡改行的,药品追溯过程中数据只可插入无法删除的特性需求;登录日志、修改日志…...
线程同步:确保多线程程序的安全与高效!
全文目录: 开篇语前序前言第一部分:线程同步的概念与问题1.1 线程同步的概念1.2 线程同步的问题1.3 线程同步的解决方案 第二部分:synchronized关键字的使用2.1 使用 synchronized修饰方法2.2 使用 synchronized修饰代码块 第三部分ÿ…...
 
关于iview组件中使用 table , 绑定序号分页后序号从1开始的解决方案
问题描述:iview使用table 中type: "index",分页之后 ,索引还是从1开始,试过绑定后台返回数据的id, 这种方法可行,就是后台返回数据的每个页面id都不完全是按照从1开始的升序,因此百度了下,找到了…...
 
【项目实战】通过多模态+LangGraph实现PPT生成助手
PPT自动生成系统 基于LangGraph的PPT自动生成系统,可以将Markdown文档自动转换为PPT演示文稿。 功能特点 Markdown解析:自动解析Markdown文档结构PPT模板分析:分析PPT模板的布局和风格智能布局决策:匹配内容与合适的PPT布局自动…...
 
Android15默认授权浮窗权限
我们经常有那种需求,客户需要定制的apk集成在ROM中,并且默认授予其【显示在其他应用的上层】权限,也就是我们常说的浮窗权限,那么我们就可以通过以下方法在wms、ams等系统服务的systemReady()方法中调用即可实现预置应用默认授权浮…...
 
Map相关知识
数据结构 二叉树 二叉树,顾名思义,每个节点最多有两个“叉”,也就是两个子节点,分别是左子 节点和右子节点。不过,二叉树并不要求每个节点都有两个子节点,有的节点只 有左子节点,有的节点只有…...
MySQL账号权限管理指南:安全创建账户与精细授权技巧
在MySQL数据库管理中,合理创建用户账号并分配精确权限是保障数据安全的核心环节。直接使用root账号进行所有操作不仅危险且难以审计操作行为。今天我们来全面解析MySQL账号创建与权限分配的专业方法。 一、为何需要创建独立账号? 最小权限原则…...
 
算法岗面试经验分享-大模型篇
文章目录 A 基础语言模型A.1 TransformerA.2 Bert B 大语言模型结构B.1 GPTB.2 LLamaB.3 ChatGLMB.4 Qwen C 大语言模型微调C.1 Fine-tuningC.2 Adapter-tuningC.3 Prefix-tuningC.4 P-tuningC.5 LoRA A 基础语言模型 A.1 Transformer (1)资源 论文&a…...
 
基于SpringBoot在线拍卖系统的设计和实现
摘 要 随着社会的发展,社会的各行各业都在利用信息化时代的优势。计算机的优势和普及使得各种信息系统的开发成为必需。 在线拍卖系统,主要的模块包括管理员;首页、个人中心、用户管理、商品类型管理、拍卖商品管理、历史竞拍管理、竞拍订单…...
 
DingDing机器人群消息推送
文章目录 1 新建机器人2 API文档说明3 代码编写 1 新建机器人 点击群设置 下滑到群管理的机器人,点击进入 添加机器人 选择自定义Webhook服务 点击添加 设置安全设置,详见说明文档 成功后,记录Webhook 2 API文档说明 点击设置说明 查看自…...
