07-生命周期详解


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

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

 一、初始化阶段

1)new Vue()都干了什么?

 new Vue()会执行Vue类的构造函数,构造函数内部会执行_init方法,所以new Vue()所干的事情其实就是_init方法所干的事情

首先,把Vue实例赋值给变量vm,并且把用户传递的options选项与当前构造函数的options属性及其父级构造函数的options属性进行合并,得到一个新的options选项赋值给$options属性,并将$options属性挂载到Vue实例上,

最后通过 extend(Vue.options.components, builtInComponents) 把一些内置组件扩展到 Vue.options.components 上,Vue 的内置组件目前 有<keep-alive><transition> 和<transition-group> 组件,这也就是为什么我们在其它组件中使用这些组件不需要注册的原因。

通过调用一些初始化函数来为Vue实例初始化一些属性,事件,响应式数据等

在所有的初始化工作都完成以后,最后,会判断用户是否传入了el选项,如果传入了则调用$mount函数进入模板编译与挂载阶段,如果没有传入el选项,则不进入下一个生命周期阶段,需要用户手动执行vm.$mount方法才进入下一个生命周期阶段。


 2)初始化函数initLifecycle -> 就是给实例初始化了一些属性,包括以$开头的供用户使用的外部属性,也包括以_开头的供内部使用的内部属性。

其主要是给Vue实例上挂载了一些属性并设置了默认值,值得一提的是挂载$parent 属性和$root属性

简单来说就是,找父亲,找到了话将其赋值vm.$parent,那么会把自身(实例)添加到父亲的children数组中。这样就确保了在子组件的$parent属性上能访问到父组件实例,在父组件的$children属性上也能访问子组件的实例。

实例的$root属性表示当前实例的根实例

 该函数的逻辑非常简单,就是给实例初始化了一些属性,包括以$开头的供用户使用的外部属性,也包括以_开头的供内部使用的内部属性。


 3)初始化函数——initEvents -> 初始化实例的事件系统

判断事件是一个浏览器原生事件还是自定义事件

模板编译的最终目的是创建render函数供挂载的时候调用生成虚拟DOM,那么在挂载阶段, 如果被挂载的节点是一个组件节点,则通过 createComponent 函数创建一个组件vnode.

父组件给子组件的注册事件中,把自定义事件传给子组件,在子组件实例化的时候进行初始化;而浏览器原生事件是在父组件中处理。

实例初始化阶段调用的初始化事件函数initEvents实际上初始化的是父组件在模板中使用v-on或@注册的监听子组件内触发的事件。

4)初始化函数——initInjections ->该函数是用来初始化实例中的inject选项的

说到inject选项,那必然离不开provide选项,这两个选项都是成对出现的,它们的作用是:允许一个祖先组件向其所有子孙后代(给孙子传数据,这也是子组件只能prop收到父组件的区别,这个是给子子孙孙,在这个家族链上的)注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效。

provide 和 inject 选项绑定的数据不是响应式的。

既然inject选项和provide选项都是成对出现的,那为什么在初始化的时候不一起初始化呢?为什么在init函数中调用initInjections函数和initProvide函数之间穿插一个initState函数呢?

因为我们通常所写datapropswatchcomputedmethod,所以inject选项接收到注入的值有可能被以上这些数据所使用到,所以在初始化完inject后需要先初始化这些数据,然后才能再初始化provide,所以在调用initInjections函数对inject初始化完之后需要先调用initState函数对数据进行初始化,最后再调用initProvide函数对provide进行初始化。

5)初始化函数——initState

initState函数就是用来初始化:Vue组件中会写一些如propsdatamethodscomputedwatch选项,我们把这些选项称为实例的状态选项,这5个选项中的所有属性最终都会被绑定到实例上,这也就是我们为什么可以使用this.xxx来访问任意属性。

首先,给实例上新增了一个属性_watchers,用来存储当前实例中所有的watcher实例,无论是使用vm.$watch注册的watcher实例还是使用watch选项注册的watcher实例,都会被保存到该属性中。

Vue中对数据变化的侦测是使用属性拦截的方式实现的,但是Vue并不是对所有数据都使用属性拦截的方式侦测变化,这是因为数据越多,数据上所绑定的依赖就会多,从而造成依赖追踪的内存开销就会很大,所以从Vue 2.0版本起,Vue不再对所有数据都进行侦测,而是将侦测粒度提高到了组件层面,对每个组件进行侦测,所以在每个组件上新增了vm._watchers属性,用来存放这个组件内用到的所有状态的依赖,当其中一个状态发生变化时,就会通知到组件,然后由组件内部使用虚拟DOM进行数据比对,从而降低内存开销,提高性能。

initState函数的所有逻辑,其实你会发现,在函数内部初始化这5个选项的时候它的顺序是有意安排的,不是毫无章法的。如果你在开发中有注意到我们在data中可以使用props,在watch中可以观察dataprops,之所以可以这样做,就是因为在初始化的时候遵循了这种顺序,先初始化props,接着初始化data,最后初始化watch


 二、模板编译阶段:

前面我们介绍了生命周期的初始化阶段,我们知道,在初始化阶段各项工作做完之后调用了vm.$mount方法(看后面解释),该方法的调用标志着初始化阶段的结束和进入下一个阶段,从官方文档给出的生命周期流程图中可以看到,下一个阶段就进入了模板编译阶段,该阶段所做的主要工作是获取到用户传入的模板内容并将其编译成渲染函数。

vue基于源码构建的有两个版本:

1.runtime only(一个只包含运行时的版本)

2.runtime + compiler(一个同时包含编译器和运行时的完整版本)

完整版本:

一个完整的Vue版本是包含编译器的,我们可以使用template选项进行模板编写。编译器会自动将template选项中的模板字符串编译成渲染函数的代码,源码中就是render函数。如果你需要在客户端编译模板 (比如传入一个字符串给 template 选项,或挂载到一个元素上并以其 DOM 内部的 HTML 作为模板),就需要一个包含编译器的版本。 如下:

 只包含运行时版本:

只包含运行时的版本拥有创建Vue实例、渲染并处理Virtual DOM等功能,基本上就是除去编译器外的完整代码。该版本的适用场景有两种:

1.我们在选项中通过手写render函数去定义渲染过程,这个时候并不需要包含编译器的版本便可完整执行。

 2.借助vue-loader这样的编译工具进行编译,当我们利用webpack进行Vue的工程化开发时,常常会利用vue-loader*.vue文件进行编译尽管我们也是利用template模板标签去书写代码,但是此时的Vue已经不需要利用编译器去负责模板的编译工作了,这个过程交给了插件去实现。

编译过程对性能会造成一定的损耗,并且由于加入了编译的流程代码,Vue代码的总体积也更加庞大(运行时版本相比完整版体积要小大约 30%)。因此在实际开发中,我们需要借助像webpackvue-loader这类工具进行编译,将Vue对模板的编译阶段合并到webpack的构建流程中,这样不仅减少了生产环境代码的体积,也大大提高了运行时的性能,一举两得。

总结:

完整版文件名:vue.js

非完整版文件名: vue.runtime.js

如果我们new Vue的时候,完整版的视图是写在HTML里或template选项的。完整版内含compiler,编译器可以把视图上的html转成DOM节点,转成html中的内容,所以体积大,不建议用,是从HTML获得视图。templete是视图的内容,可以直接包含html内容,常直接放在视图层,用在完整版就直接把html显示在视图

 非完整版:

非完整版中的html只是字符串,且不能从html中获取视图,但是可以通过webpack内的vue-loader,来调用pomliler,把其转成h()函数,用户下载的js文件体积变小了。

render是非完整版常用的,常把视图写在rander函数中,用h来创造标签,templete的内容放在vue文件中,vue-loader可以把内容被转换成render(h){h(‘div’)},render函数为render:h=>h(demo)。

如render函数中的参数为App.vue。App.vue中就放了一个router-view,用作路由组件切换的容器,每一个组件里面就有自己的template,这就印证了上面说的,template内容放在vue文件中,也就是App.vue中,只不过最后使用vue-loader去解析转为render函数。

注意:这个$mount是挂载阶段,render是编译->编译为虚拟dom

完整版和非完整版两种版本$mount方法有着不一样的作用:

它们的区别在于在$mount方法中是否进行了模板编译。在只包含运行时版本的$mount方法中获取到DOM元素后直接进入挂载阶段,而在完整版本的$mount方法中是先将模板进行编译,然后回过头调只包含运行时版本的$mount方法进入挂载阶段

综上:这个$mount方法就是编译结束的标志。完整版的$mount是从用户传入的el选项和template选项中获取到用户传入的内部或外部模板,然后将获取到的模板编译成渲染函数。然后再调用运行时版本的$mount方法进入挂载阶段。

最后总结:不管是完整版还是运行时版本,完整版是自己用编译器去编译,并且用html作为模板;运行版本则靠vue-loader靠的是webpack去解析,组件(.vue文件)内部的模板在构建时编译成渲染函数,所以是不需要编译的。最终不管完整版是通过自带的编译器还是非完整版靠webpack的插件,目的都会变成渲染函数


 三、挂载阶段

模板编译阶段完成之后,接下来就进入了挂载阶段,从官方文档给出的生命周期流程图中可以看到,挂载阶段所做的主要工作是创建Vue实例并用其替换el选项对应的DOM元素,同时还要开启对模板中数据(状态)的监控,当数据(状态)发生变化时通知其依赖进行视图更新。

执行渲染函数vm._render()得到一份最新的VNode节点树,然后执行vm._update()方法对最新的VNode节点树与上一次渲染的旧VNode节点树进行对比并更新DOM节点(即patch操作),完成一次渲染。

我们将挂载阶段所做的工作分成两部分进行了分析,第一部分是将模板渲染到视图上,第二部分是开启对模板中数据(状态)的监控。两部分工作都完成以后挂载阶段才算真正的完成了。

四、销毁阶段

当调用了vm.$destroy方法,Vue实例就进入了销毁阶段,该阶段所做的主要工作是将当前的Vue实例从其父级实例中删除,取消当前实例上的所有依赖追踪并且移除实例上的所有事件监听器。也就是说,当这个阶段完成之后,当前的Vue实例的整个生命流程就全部走完了

 

 

原文地址:https://www.cnblogs.com/haoqiyouyu/p/14645022.html