请介绍一下Vue的生命周期?你为什么还要问这么愚蠢的问题?考察你的熟练程度,考察你的深度,考察你的见识,你是这样认为的吗?关于vue的生命周期,有的可以命名下面的钩子函数,有的连这些钩子函数都不能命名。这才是我真正需要补充的。是的,因为这些钩子函数只是Vue完整生命周期中的冰山一角源码地址:src/shared/constants.js-9行exportconstLIFECYCLE_HOOKS=['beforeCreate','created','beforeMount','mounted','beforeUpdate','updated','beforeDestroy','destroyed','activated','deactivated','errorCaptured','serverPrefetch']Vue的完整生命周期大致可以分为四个阶段初始化阶段:InitializeforVueinstanceTemplatecompilationstageforsomeevents,attributes,responsivedata:将我们编写的模板编译成渲染函数rendermount阶段:将模板渲染到真实的DOM节点,并进行update当数据发生变化时运行和销毁阶段:从父组件中删除组件实例,取消依赖监听和事件监听四个阶段。看一张图(图片来自Vue中文社区)下面我们看一下源码。一旦你展开每个阶段做了什么,注意每个生命周期钩子函数被调用的时间,前后做了什么。初始化阶段newVue()这个阶段首先要做的是创建一个带有new对象的Vue实例newVue({el:'#app',store,router,render:h=>h(App)})可以使用new,所以它必须有一个构造函数。我们看一下源码地址:src/core/instance/index.js-8行functionVue(options){if(process.env.NODE_ENV!=='production'&&!(thisinstanceofVue)){warn('Vue是一个构造函数,应该用`new`关键字调用')}this._init(options)}initMixin(Vue)可以看出newVue()的key是_init(),我们拿看看它从哪里来,做了什么?_init()源码地址:src/core/instance/init.js-第15行,我这里去掉了一些环境判断。主要过程是合并和配置,主要是集成一些内置组件,,,directive,filter,本文开头的hook函数名列表等.合并到Vue.options中调用一些初始化函数。我在评论里写下这里具体的初始化是什么。为了触发生命周期钩子,beforeCreate和createdfinally调用$mount挂载进入下一阶段(options?:Object){//保存当前实例constvm:Component=this//合并配置if(options&&options._isComponent){//将子组件依赖于父组件的props和listeners挂载到options中,并且指定组件的$optionsinitInternalComponent(vm,options)}else{//将我们传入的选项与当前构造函数和父级的选项合并,并挂载到原型上vm.$options=mergeOptions(resolveConstructorOptions(vm.构造函数),选项||{},vm)}vm._self=vminitLifecycle(vm)//初始化实例的属性和数据:$parent,$children,$refs,$root,_watcher...等initEvents(vm)//初始化事件:$on,$off,$emit,$onceinitRender(vm)//初始化渲染:render,mixincallHook(vm,'beforeCreate')//调用生命周期钩子函数initInjections(vm)//初始化注入initState(vm)//初始化componentdata:props,data,methods,watch,computedinitProvide(vm)//初始化提供callHook(vm,'created')//调用生命周期钩子函数if(vm.$options.el){//如果el是通过,会调用$mount进入模板编译挂载阶段//如果没有上传,需要手动执行$mount进入下一阶段vm.$mount(vm.$options.el)}}}这里需要记住上面的流程描述,合并了哪些配置,初始化了什么,两个生命周期钩子函数是什么时候调用的,最后调用$mount挂载,进入下一阶段of模板编译阶段,先看图了解这个阶段做了什么$mount()源码地址:dist/vue.js-11927行Vue.prototype.$mount=function(el,hydrating){el=el&&查询(el);varoptions=this.$options;//如果没有渲染if(!options.render){vartemplate=options.template;//再次判断,是否有模板if(template){if(typeoftemplate==='string'){if(template.charAt(0)==='#'){template=idToTemplate(template);}}elseif(template.nodeType){template=template.innerHTML;}else{returnthis}//再判断一下,有没有el}elseif(el){template=getOuterHTML(el);}}returnmount.call(this,el,hydrating)};模板编译,需要注意的是判断顺序,我们来看这段代码{{name}}
因为源码是先判断render是否存在。如果存在,直接使用render函数。如果不是,则判断模板和el。如果有模板,它不会关心el。所以优先级顺序是:render>template>el因为不管是el挂载还是template最后都会编译成render函数,如果已经有render函数,跳过前面的编译。templatecompilation是怎么编译的?渲染是怎么来的?有很多细节。详细过程可以参考我的另一篇文章,完整的过程有源码。render函数是怎么来的?深入理解Vue中的模板编译完render函数后,就会进入mount阶段。在mount阶段,先看图了解这个阶段做了什么。从图中也可以看出。这个阶段主要有两件事要做。根据render返回的虚拟DOM创建一个真实的DOM节点,插入到视图中,完成渲染。响应式处理模板中的数据或状态。mountComponent()下面看一下挂载的源码。这里删掉了一点环境判断,方便查看主要流程这里主要做的是调用钩子函数beforeMount调用_update()方法对新旧虚拟DOM和newWatcher打补丁做响应式处理模板数据,然后调用钩子函数mounted源码地址:src/core/instance/lifecycle.js-第141行$el=el//判断是否有渲染函数renderif(!vm.$options.render){//如果没有,默认创建一个注释节点vm.$options.render=createEmptyVNode}//调用lifecyclehookfunctioncallHook(vm,'beforeMount')letupdateComponentupdateComponent=()=>{//调用_updatePatch(即Diff)将render返回的虚拟DOM打到真实DOM上,这里是第一次渲染vm._update(vm._render(),hydrating)}//为当前组件实例设置一个观察者来监听updateComponent函数获取的数据,下面介绍newWatcher(vm,updateComponent,noop,{//当更新被触发new的时候会调用before(){//判断DOM是否挂载,也就是说第一次渲染卸载的时候不会执行if(vm._isMounted&&!vm._isDestroyed){//调用生命周期钩子函数callHook(vm,'beforeUpdate')}}},true/*isRenderWatcher*/)hydrating=false//没有旧的vnode,说明是第一次渲染if(vm.$vnode==null){vm._isMounted=true//调用生命周期钩子函数callHook(vm,'mounted')}returnvm}Watcher关于响应式原理,可以看我另一篇文章有??详细介绍,所以我不会把它移到这里。深入浅出地解释一下Vue的响应式原理源码analysis_update()然后看入口打补丁。Diff算法源码的完整流程分析,可以看我的另一篇文章,里面有详细介绍。Vue2和Vue3的区别源码地址:src/core/instance/lifecycle.js-59行$el//当前组件根节点constprevVnode=vm._vnode//旧vnodevm._vnode=vnode//更新旧vnode//如果它是第一次渲染if(!prevVnode){//修补vnode以创建真正的DOM,安装在vm.$elvm.$el=vm.__patch__(vm.$el,vnode,hydrating,false/*removeOnly*/)}else{//修改时,比较新旧vnode,返回修改后的真实DOMvm.$el=vm.__patch__(prevVnode,vnode)}//删除旧根节点的引用if(prevEl)prevEl.__vue__=null//更新当前根节点的引用if(vm.$el)vm.$el.__vue__=vm//更新父节点的引用if(vm.$vnode&&vm.$parent&&vm.$vnode===vm.$parent._vnode){vm.$parent.$el=vm.$el}}销毁阶段先看图了解这个阶段做了什么,也就是调用vm的时候。$destroy()方法,Vue进入了销毁阶段。我们来看看组件销毁过程中做了什么,又是如何销毁的?$destroy()这个阶段比较简单,源码不多,主要是:调用生命周期钩子函数beforeDestory从父组件中删除当前组件,移除当前组件中的所有观察者(依赖跟踪),删除数据对象的引用,delete虚拟DOM调用生命周期钩子函数destroyed关闭所有事件监听,删除当前根组件的引用,删除父级的引用。我把每一行写在注释里,注意源码地址:src/core/instance/lifecycle.js-Line97Vue.prototype.$destroy=function(){constvm:Component=this//如果实例正在被销毁,直接跳过if(vm._isBeingDestroyed){return}//调用生命周期钩子函数callHook(vm,'beforeDestroy')//更新销毁过程的状态vm._isBeingDestroyed=true//获取parentconstparent=vm.$parent//如果parent存在,且parent没有被销毁,则不是抽象组件而是真实组件(