通过 debugger vue 源码 观察得出的vue生命周期流程
对vue的生命周期进行深入了解 (*/ω\*)

版本:vue2.6.11 完整版本

附官方配图

一切的一切要从vue文件加载说起

init

无论是模块化引入,还是script标签加载,都会引入vue的bundle文件,在进入生命周期之前,vue会执行如下几个方法
往vue的构造函数上挂载之后需要用到的变量,往原型上挂载要用到的方法
修改push,pop等数组操作方法,使用包装器包装

// initGlobalAPI
initMixin(Vue);
stateMixin(Vue);
eventsMixin(Vue);
lifecycleMixin(Vue);
renderMixin(Vue);

new Vue(options)

新建vue实例的时候,构造函数内只有一个入口this._init(options);

mergeOptions(constructor.options, options, vm)

将initGlobalApi中初始化的构造函数上的属性和传入的属性做一次merge,此时,一些我们平常没有注入的Component,keep-alive,transition,transition-group三个内置组件也是在这里被merge进我们的option的。值得一提的是,这里的merge不是简单的对象合并,针对不同的option,会采用不同的合并策略,比如data执行后合并成新的data函数,生命周期钩子,则被推入对应生命周期数组中

beforeCreate

initLifecycle(vm);
initEvents(vm);
initRender(vm);
callHook(vm, 'beforeCreate');

执行beforeCreate钩子,此时的数据还不是响应式,真实dom也没有挂载到页面,页面还是带vue语法的模板

数据监测

从data函数开始,data.call(vm, vm)得到data对象,对data对象的每个属性进行监测,为每个属性新建一个dep对象const dep = new Dep(),通过Obejct.defineProperty设置对应属性的get,set,在get中进行依赖收集,需要收集的地方会将Dep.target加到dep.sub中dep.addSub(Dep.target),在被set时,通过dep.notify() => watcher.cb()来通知模板更新,如果属性是数组,其数组操作方法在初始化时已经被替换,则在各个操作方法中进行通知。如果当前key对应的属性仍不是基本类型,则继续进行深度遍历,知道所有属性被监听。除了data这一阶段还包括对watcher和computed的处理。

Create

这一阶段还包括了父组件provide和子组件inject的初始化处理,数据监听完毕,触发create钩子

initInjections(vm); // resolve injections before data/props
initState(vm);
initProvide(vm); // resolve provide after data/props
callHook(vm, 'created');

模板编译阶段 CompileToFn

vue分两种版本,一种是包含模板编译大一点的版本,一种是vue.runtime.js,只包含运行时的版本,在使用webpack构建时常使用vue-loader来预处理模板,因此到页面上只需引入运行时的vue版本就好了,而没有使用模板编译的话,只能引入全量的vue了。所谓模板编译,其实就是template到render函数的过程,中间通过ast转化,通过正则匹配来确定是普通的html语法还是vue的特殊语法从而进行不同的特殊处理,最后得到一个类似这样的render函数

(function anonymous(
) {
with(this){return _c('div',{attrs:{"id":"demo"}},[_c('h1',[_v("Latest Vue.js Commits")]),_v(" "),_l((branches),function(branch){return [_c('input',{directives:[{name:"model",rawName:"v-model",value:(currentBranch),expression:"currentBranch"}],attrs:{"type":"radio","id":branch,"name":"branch"},domProps:{"value":branch,"checked":_q(currentBranch,branch)},on:{"change":function($event){currentBranch=branch}}}),_v(" "),_c('label',{attrs:{"for":branch}},[_v(_s(branch))])]}),_v(" "),_c('p',[_v("vuejs/vue@"+_s(currentBranch))]),_v(" "),_c('ul',_l((commits),function(record){return _c('li',[_c('a',{staticClass:"commit",attrs:{"href":record.html_url,"target":"_blank"}},[_v(_s(record.sha.slice(0, 7)))]),_v("\n          - "),_c('span',{staticClass:"message"},[_v(_s(_f("truncate")(record.commit.message)))]),_c('br'),_v("\n          by "),_c('span',{staticClass:"author"},[_c('a',{attrs:{"href":record.author.html_url,"target":"_blank"}},[_v(_s(record.commit.author.name))])]),_v("\n          at "),_c('span',{staticClass:"date"},[_v(_s(_f("formatDate")(record.commit.author.date)))])])}),0)],2)}
})

with语法执行了当前作用域环境,_c其实就是常见的createElement函数

模板编译完成,得到render函数,触发beforeMounted钩子

vnodeToElm,mounted钩子触发

函数执行生成的是vnode,再经由vm._update(vnode, hydrating)处理,进入vm.__patch__(vm.$el, vnode, hydrating, false)处理,第一次不会进入patchVnode函数,进入生成分支emptyNodeAt(elm),生成最外层的父节点,再执行createElm阶段,将当前elm设置成document.createElement(tagName),再继续进行createChildren,对节点的子节点重复调用createElm,所得的真实dom会通过parent.insertBefore或者parent.appendChild添加到父级真实dom中,最后再移除之前所有的未编译节点,呈现最终的页面(vnode.elm可以看到所有的真实节点)
mountComponent执行完毕,触发mounted钩子,页面上已有真实dom,但是尚未渲染。

nextTick异步任务

渲染流程
根据浏览器兼容性,promise => mutationObserver => setTimeout
微任务可以确保执行时间点会在会在渲染完毕之前,所以在promise => mutationObserver实现的情况下只会进行一次渲染,而setTimeout(fn, 0),无法确保执行时间是在渲染后还是渲染之前,所以可能会造成重复渲染,造成跳帧的感觉。

beforeUpdate钩子

在mounted之后,data数据发生改变时调用,其有一个专属的组件级的watcher,在数据变化时会被调用before函数,在所有设置的watcher之前,触发beforeUpdate钩子。

updated钩子

在beforeUpdate钩子之后,对所有的update钩子进行执行nextTick(flushSchedulerQueue) -> callHook(vm, 'update'),如果设置了,则每次在下一次重渲染之前,update钩子会被调用。

beforeDestory钩子

destory可以通过vm.$destory()来主动触发,其并不会影响页面的展示,但是会清理与其它实例的连接,解绑(vm.deps[i].removeSub())当前实例的全部指令及事件监听器,然后执行vm.__patch__(vm._vnode, null),将当前vnode置空,并在其中会触发子组件的destory方法,和mounted一样,destory钩子也是先子组件后父组件

Destory钩子

所有销毁动作完成之后,触发destroyed钩子,

activated钩子 && deactivated钩子

涉及keep-alive没有深入研究,大概是组件被复用和缓存组件被销毁时触发


总结

1、js加载并执行,混入必要的属性和方法,声明常用工具类方法
2、实例化,属性合并,初始化解析event事件绑定的方法
3、触发beforeCreate
4、进行数据监测,依赖收集
5、触发craete
6、进行模板编译,将模板转化为render函数
7、触发beforeMounted
8、执行render函数,生成vnode,根据vnode创建真实dom,挂载到指定节点
9、触发mounted钩子
其他
10、beforeUpdate执行所在的函数在beforeMounted和mounted之前已经生成了,mounted之后的数据变动都会触发beforeUpdate
11、watcher.run()
12、模板更新结束,触发updated钩子
13、beforeDestory组件销毁时触发,也可主动触发,进行事件解绑,依赖移除,vnode清空,执行顺序是先子组件后父组件
14、destory组件销毁完毕触发
15、activated钩子 && deactivated钩子 缓存组件复用和缓存组件被销毁时触发