【Vue2.0源码学习】生命周期篇-初始化阶段(new Vue)

文章目录

    • 1.综述
      • 1.1 前言
      • 1.2 生命周期流程图
      • 1.3 总结
    • 2.初始化阶段
      • 2.1 前言
      • 2.2 new Vue()都干了什么
      • 2.3 合并属性
      • 2.4 callHook函数如何触发钩子函数
      • 2.5 总结
      • 2.5 总结

1.综述

1.1 前言

Vue中,每个Vue实例从被创建出来到最终被销毁都会经历一个过程,就像人一样,从出生到死亡。在这一过程里会发生许许多多的事,例如设置数据监听,编译模板,组件挂载等。在Vue中,把Vue实例从被创建出来到最终被销毁的这一过程称为Vue实例的生命周期,同时,在Vue实例生命周期的不同阶段Vue还提供了不同的钩子函数,以方便用户在不同的生命周期阶段做一些额外的事情。那么,接下来的几篇文章我们就从源码角度深入剖析一下一个Vue实例在从生到死的生命周期里到底都经历了些什么,每个阶段都做了哪些事情。

1.2 生命周期流程图

下图是Vue官网给出的Vue实例的生命周期流程图,如下:

在这里插入图片描述

从图中我们可以看到,Vue实例的生命周期大致可分为4个阶段:

  • 初始化阶段:为Vue实例上初始化一些属性,事件以及响应式数据;
  • 模板编译阶段:将模板编译成渲染函数;
  • 挂载阶段:将实例挂载到指定的DOM上,即将模板渲染到真实DOM中;
  • 销毁阶段:将实例自身从父组件中删除,并取消依赖追踪及事件监听器;

1.3 总结

本篇文章是生命周期篇的开篇综述,借用Vue官网的生命周期流程图介绍了一个Vue实例的生命周期大致可分为四个阶段,分别是初始化阶段、模板编译阶段、挂载阶段、销毁阶段。接下来的几篇文章我们就从这个流程图为基础,自上到下,从每个阶段入手,深入分析在每个阶段里都干了些什么。

2.初始化阶段

2.1 前言

上篇文章中介绍了Vue实例的生命周期大致分为4个阶段,那么首先我们先从第一个阶段——初始化阶段开始入手分析。从生命周期流程图中我们可以看到,初始化阶段所做的工作也可大致分为两部分:第一部分是new Vue(),也就是创建一个Vue实例;第二部分是为创建好的Vue实例初始化一些事件、属性、响应式数据等。接下来我们就从源码角度来深入分析一下初始化阶段所做的工作及其内部原理。

2.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 = this
    vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
    )
    vm._self = vm
    initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(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函数是如何触发生命周期的钩子的问题。

2.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_TYPESASSET_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.extends
  if (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 key
  for (key in parent) {
    mergeField(key)
  }
  for (key in child) {
    if (!hasOwn(parent, key)) {
      mergeField(key)
    }
  }
  function mergeField (key) {
    const strat = strats[key] || defaultStrat
    options[key] = strat(parent[key], child[key], vm, key)
  }
  return options
}

可以看出,mergeOptions函数的 主要功能是把 parentchild 这两个对象根据一些合并策略,合并成一个新对象并返回。首先递归把 extendsmixins 合并到 parent 上,

 const extendsFrom = child.extends
  if (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 函数,它不是简单的把属性从一个对象里复制到另外一个对象里,而是根据被合并的不同的选项有着不同的合并策略。例如,对于datadata的合并策略,即该文件中的strats.data函数;对于watchwatch的合并策略,即该文件中的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 函数,一旦 parentchild 都定义了相同的钩子函数,那么它们会把 2 个钩子函数合并成一个数组。

那么问题来了,为什么要把相同的钩子函数转换成数组呢?这是因为Vue允许用户使用Vue.mixin方法(关于该方法会在后面章节中介绍)向实例混入自定义行为,Vue的一些插件通常都是这么做的。所以当Vue.mixin和用户在实例化Vue时,如果设置了同一个钩子函数,那么在触发钩子函数时,就需要同时触发这个两个函数,所以转换成数组就是为了能在同一个生命周期钩子列表中保存多个钩子函数。

2.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,我们说过,每个生命周期钩子名称都对应了一个钩子函数数组。然后遍历该数组,将数组中的每个钩子函数都执行一遍。

2.5 总结

本篇文章介绍了生命周期第一个阶段——初始化阶段中所做的第一件事:new Vue()

首先,分析了new Vue()时其内部都干了些什么。其主要逻辑就是:合并配置,调用一些初始化函数,触发生命周期钩子函数,调用$mount开启下一个阶段。

接着,就合并属性进行了详细介绍,知道了对于不同的选项有着不同的合并策略,并挑出钩子函数的合并策略进行了分析。

最后,分析了callHook函数的源码,知道了callHook函数如何触发钩子函数的。

取到需要触发的钩子名称所对应的钩子函数数组handlers,我们说过,每个生命周期钩子名称都对应了一个钩子函数数组。然后遍历该数组,将数组中的每个钩子函数都执行一遍。

2.5 总结

本篇文章介绍了生命周期第一个阶段——初始化阶段中所做的第一件事:new Vue()

首先,分析了new Vue()时其内部都干了些什么。其主要逻辑就是:合并配置,调用一些初始化函数,触发生命周期钩子函数,调用$mount开启下一个阶段。

接着,就合并属性进行了详细介绍,知道了对于不同的选项有着不同的合并策略,并挑出钩子函数的合并策略进行了分析。

最后,分析了callHook函数的源码,知道了callHook函数如何触发钩子函数的。

接下来后面几篇文章将对调用的这些初始化函数进行逐个分析。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/72716.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

docker搭建LNMP

docker安装 略 下载镜像 nginx:最新版php-fpm:根据自己需求而定mysql:根据自己需求定 以下是我搭建LNMP使用的镜像版本 rootVM-12-16-ubuntu:/docker/lnmp/php/etc# docker images REPOSITORY TAG IMAGE ID CREATED SIZE mysql 8.0…

微服务——数据同步

问题分析 mysql和redis之间有数据同步问题&#xff0c;ES和mysql之间也有数据同步问题。 单体项目可以在crud时就直接去修改&#xff0c;但在微服务里面不同的服务不行。 方案一 方案二 方案三 总结 导入酒店管理项目 倒入完成功启动后可以看见数据成功获取到了 声明队列和…

模拟实现消息队列(以 RabbitMQ 为蓝本)

目录 1. 需求分析1.1 介绍一些核心概念核心概念1核心概念2 1.2 消息队列服务器&#xff08;Broker Server&#xff09;要提供的核心 API1.3 交换机类型1.3.1 类型介绍1.3.2 转发规则&#xff1a; 1.4 持久化1.5 关于网络通信1.5.1 客户端与服务器提供的对应方法1.5.2 客户端额外…

网络基础-PosixAPI

文章目录 一.网络常用接口Linux协议栈与posix api的关系 背景1.1 socket1.8 close服务端posix接口1.2 bind1.3 listen1.4 accept1.5 connect1.6 send1.7 recv总结 二. 修改句柄属性2.1 fctl2.2 特殊作用2.2.1 设置阻塞非阻塞2.2.1 实例2.2.2设置获取记录锁2.2.2 实例 三、文件操…

数据库操作不再困难,MyBatis动态Sql标签解析

系列文章目录 MyBatis缓存原理 Mybatis的CachingExecutor与二级缓存 Mybatis plugin 的使用及原理 MyBatis四大组件Executor、StatementHandler、ParameterHandler、ResultSetHandler 详解 MyBatisSpringboot 启动到SQL执行全流程 数据库操作不再困难&#xff0c;MyBatis动态S…

n-皇后问题

希望这篇题解对你有用&#xff0c;麻烦动动手指点个赞或关注&#xff0c;感谢您的关注 不清楚蓝桥杯考什么的点点下方&#x1f447; 考点秘籍 想背纯享模版的伙伴们点点下方&#x1f447; 蓝桥杯省一你一定不能错过的模板大全(第一期) 蓝桥杯省一你一定不能错过的模板大全…

Python教程(8)——一文弄懂Python字符串操作(下)

Python字符串操作 字符串常用方法字符串更多方法介绍 字符串常用方法 字符串在编程中是一种不可或缺的数据类型&#xff0c;它在文本和字符数据时提供了丰富而强大的功能。掌握了字符串的使用方法&#xff0c;你能够更加便捷地进行文本处理、数据操作、用户交互等任务&#xf…

opencv图片换背景色

#include <iostream> #include<opencv2/opencv.hpp> //引入头文件using namespace cv; //命名空间 using namespace std;//opencv这个机器视觉库&#xff0c;它提供了很多功能&#xff0c;都是以函数的形式提供给我们 //我们只需要会调用函数即可in…

文献阅读:AnnoLLM: Making Large Language Models to Be Better Crowdsourced Annotators

文献阅读&#xff1a;AnnoLLM: Making Large Language Models to Be Better Crowdsourced Annotators 1. 文章简介2. 方法介绍3. 实验考察 1. 实验结果2. 消解实验3. Consistency & Stability 4. 结论 & 思考 文献链接&#xff1a;https://arxiv.org/abs/2303.16854 …

GrapeCity Documents for Excel, Java Edition Crack

GrapeCity Documents for Excel, Java Edition Crack 增加了对SpreadJS.sjs文件格式的支持&#xff1a; 更快地将大型Microsoft Excel文件转换为.sjs格式。 使用较小的占用空间保存导出的文件。 将Excel/SpreadJS功能导入SpreadJS/从SpreadJS导出。 从.sjs文件中压缩的JSON文件…

@Param详解

文章目录 背景什么是ParamParam的使用方法使用方法&#xff1a;遇到的问题及因Param解决了什么问题使用与不使用对比 Param是如何进行映射的总结 背景 最近在开发过程中&#xff0c;在写mapper接口是在参数前加了Param注解&#xff0c;但是在运行的时候就会报错&#xff0c;说…

设备取电芯片LDR6328Q

2021年5月&#xff0c;USB-IF 协会发布了全新的USB PD3.1规范&#xff0c;该规范将快充功率上限从100 W提升至240W&#xff08;支持Extended Power Range&#xff0c;简称EPR&#xff09;。充电功率的提升也让USB PD的应用从手机、笔记本电脑&#xff0c;扩展到便携式设备、物联…

教育行业文件协作的最佳实践分享!

在教育工作中&#xff0c;经常需要进行文件协作&#xff0c;无论是师生间还是老师与老师之间。目前最常用的文件协作方式就是通过社交工具或者邮件进行文件共享。 这种协作方式的缺点 1、大文件传输不便&#xff1a;这种协作方式依托于社交工具&#xff0c;对于大文件传输并不…

百日筑基篇——python爬虫学习(一)

百日筑基篇——python爬虫学习&#xff08;一&#xff09; 文章目录 前言一、python爬虫介绍二、URL管理器三、所需基础模块的介绍1. requests2. BeautifulSoup1. HTML介绍2. 网页解析器 四、实操1. 代码展示2. 代码解释1. 将大文件划分为小的文件2. 获得结果页面的url3. 获取结…

opsForHash() 与 opsForValue 请问有什么区别?

&#x1f449;&#xff1a;&#x1f517;官方API参考手册 如图&#xff0c;opsForHash()返回HashOperations<K,HK,HV>但是 opsForValue()返回ValueOperations<K,V>… 区别就是opsForHash的返回值泛型中有K,HK,HV,其中K是Redis指定的某个数据库里面某一个关键字(由…

Flutter BottomSheet 三段式拖拽

BottomSheetBehavior 追踪 BottomSheet系统默认实现效果准备要实现的功能点&#xff1a;定义三段式状态&#xff1a;BottomSheetBehavoir阀值定义1. 未达到滚动阀值&#xff0c;恢复状态2. 达到滚动阀值&#xff0c;更新状态 前面倒是有讲过Android原生的BottomSheetBehavior&a…

谈谈我的感受

虽然清楚知识需要靠时间沉淀&#xff0c;但在看到自己做不出来的题别人会做&#xff0c;自己写不出的代码别人会写时还是会感到焦虑怎么办&#xff1f; 感受 当我们比别人弱的时候&#xff0c;可能会感到挫败、失落、沮丧或无力。这种感受往往是因为我们与自己设定的标准或别人…

时序预测-Informer简介

Informer介绍 1. Transformer存在的问题 Informer实质是在Transformer的基础上进行改进&#xff0c;通过修改transformer的结构&#xff0c;提高transformer的速度。那么Transformer有什么样的缺点&#xff1a; &#xff08;1&#xff09;self-attention的平方复杂度。self-…

ruoyi-cloud微服务新建子模块

目录 相关文章1、复制system模块2、在modules下的 pom.xml文件中添加子模块 test3、进入 test模块修改 pom.xml4、修改对应的包名、目录名和启动应用程序为test5、修改bootstrap.yml文件中的端口号和应用名称6、nacos中克隆 system-dev.yml的配置&#xff0c;修改名称为 test-d…

章节7:XSS检测和利用

章节7&#xff1a;XSS检测和利用 测试payload <script>alert(XSS)</script> <script>alert(document.cookie)</script> ><script>alert(document.cookie)</script> ><script>alert(document.cookie)</script> &qu…