作者:王聪本文旨在介绍vue实例化挂载dom的整体路线,部分细节省略。从newVue()开始,一切从newVue()开始,所以从这一点开始,探究这个过程中发生了什么。从源码中找到Vue构造函数的声明,src/core/instance/index.jsimport{initMixin}from'./init'import{stateMixin}from'./state'import{renderMixin}from'./render'从'./events'导入{eventsMixin}从'./lifecycle'导入{warn}从'../util/index'functionVue(选项){if(process.env.NODE_ENV!=='production'&&!(thisinstanceofVue)){warn('Vue是一个构造函数,应该使用`new`关键字调用')}this._init(options)}initMixin(Vue)stateMixin(Vue)eventsMixin(Vue)lifecycleMixin(Vue)renderMixin(Vue)exportdefaultVue是以非常简洁的函数工厂模式声明的构造函数。内部进行逻辑判断:构造函数调用必须有new关键字。然后执行this._init(options),这个初始化函数就是vue初始化的开始。Vue.prototype._init()this._init()什么时候声明?通过以下五个初始化函数的执行,将大量的属性和函数添加到Vue原型链中。this._init()实际上调用了原型链上的Vue.prototype._init()函数。initMixin(Vue)stateMixin(Vue)eventsMixin(Vue)lifecycleMixin(Vue)renderMixin(Vue)Vue.prototype._init函数添加到initMixin(Vue)的原型链中。在src/core/instance/init.js中定义Vue.prototype._init=function(options?:Object){constvm:Component=this//auidvm._uid=uid++letstartTag,endTag/*istanbulignoreif*/if(process.env.NODE_ENV!=='production'&&config.performance&&mark){startTag=`vue-perf-start:${vm._uid}`endTag=`vue-perf-end:${vm._uid}`mark(startTag)}//避免这种情况被观察到的标志vm._isVue=true//合并选项if(options&&options._isComponent){//优化内部组件实例化//因为动态选项合并是非常慢,并且//内部组件选项都不需要特殊处理。initInternalComponent(vm,options)}else{vm.$options=mergeOptions(resolveConstructorOptions(vm.constructor),options||{},vm)}/*伊斯坦布尔忽略其他*/if(process.env.NODE_ENV!=='生产n'){initProxy(vm)}else{vm._renderProxy=vm}//暴露真实的自我vm._self=vminitLifecycle(vm)initEvents(vm)initRender(vm)callHook(vm,'beforeCreate')initInjections(vm)//解析数据/道具之前的注入initState(vm)initProvide(vm)//解析数据/道具之后的注入callHook(vm,'created')/*istanbulignoreif*/if(process.env.NODE_ENV!=='production'&&config.performance&&mark){vm._name=formatComponentName(vm,false)mark(endTag)measure(`vue${vm._name}init`,startTag,endTag)}if(vm.$options.el){vm.$mount(vm.$options.el)}}}_init函数内部通过上述调用初始化函数的方式完成了具体模块的初始化。声明钩子的调用也是第一次出现在这里。通过分析这些函数调用的先后顺序,可以更好的理解官方文档中提到的各个生命周期钩子函数的触发时机。initLifecycle(vm)initEvents(vm)initRender(vm)callHook(vm,'beforeCreate')initInjections(vm)//解决数据前注入/propsinitState(vm)initProvide(vm)//解决数据后提供/propscallHook(vm,'created')在_init()函数的末尾,执行vm.$mount()。vm代表当前的vue实例,也就是调用构造函数后的this点。**$mount**是什么时候在vue上声明的?这次没有在上面的初始化函数中声明。因为$mount方法的实现与平台和构建方法有关,所以在不同的构建入口文件中有不同的定义。在Vue.prototype.$mount原型上声明的$mount方法定义在src/platform/web/runtime/index.js中。此方法将在仅运行时版本和带有编译器的完整版本中重用。Vue版本说明。//publicmountmethodVue.prototype.$mount=function(el?:string|Element,hydrating?:boolean):Component{el=el&&inBrowser?query(el):undefinedreturnmountComponent(this,el,hydrating)}现在在上面的Vue.prototype._init函数中调用了vm.$mount函数,实际执行的是mountComponent(this,el,hydrating)函数。mountComponent定义在目录src/core/instance/lifecycle.js中exportfunctionmountComponent(vm:Component,el:?Element,hydrating?:boolean):Component{vm.$el=elif(!vm.$options.render){vm.$options.render=createEmptyVNodeif(process.env.NODE_ENV!=='production'){/*伊斯坦布尔忽略if*/if((vm.$options.template&&vm.$options.template.charAt(0)!=='#')||vm.$options.el||el){warn('您正在使用Vue的运行时构建,其中模板'+'编译器不可用。要么预将模板编译成'+'渲染函数,或使用编译器包含的构建。',vm)}else{warn('无法挂载组件:模板或渲染函数未定义。',vm)}}}callHook(vm,'beforeMount')letupdateComponent/*istanbulignoreif*/if(process.env.NODE_ENV!=='production'&&config.performance&&mark){updateComponent=()=>{constname=vm._nameconstid=vm._uidconststartTag=`vue-perf-start:${id}`constendTag=`vue-perf-end:${id}`mark(startTag)constvnode=vm._render()mark(endTag)measure(`vue${name}render`,startTag,endTag)mark(startTag)vm._update(vnode,hydrating)mark(endTag)measure(`vue${name}patch`,startTag,endTag)}}else{updateComponent=()=>{vm._update(vm._render(),hydrating)}}//我们将其设置为vm。_watcher在观察者的构造函数中//因为观察者的初始补丁可能会调用$forceUpdate(例如在子//组件的挂载钩子中),这依赖于已经定义的vm._watchernewWatcher(vm,updateComponent,noop,{before(){if(vm._isMounted&&!vm._isDestroyed){callHook(vm,'beforeUpdate')}}},true/*isRenderWatcher*/)hydrating=false//手动挂载的实例,调用mountedonself//mounted在其插入的钩子中为渲染创建的子组件调用if(vm.$vnode==null){vm._isMounted=truecallHook(vm,'mounted')}returnvm}摒弃了源码中的性能测试和异常警告代码,该函数内部做了以下事情:updateComponent,noop,{...callHook(vm,'beforeUpdate')}})callHook(vm,'mounted'),调用这里挂载的生命周期钩子newWatcher()实例化Watcher类,内部逻辑不会去更深入一点,这里你只需要知道在实例化过程中调用了作为参数传入的updateComponent函数,从这个函数的声明来看,它实际上执行了vm._update(vm._render(),hydrating)这个函数首先,Vue原型上定义了vm._update和vm._render这两个方法。Vue.prototype._renderVue的_render方法用于将实例渲染成虚拟Node。它是在执行renderMixin(Vue)时声明的。它的定义在src/core/instance/render.js文件中Vue.prototype._render=function():VNode{constvm:Component=thisconst{render,_parentVnode}=vm.$options//reset_renderedflagon用于重复插槽检查的插槽if(process.env.NODE_ENV!=='production'){for(constkeyinvm.$slots){//$flow-disable-linevm.$slots[key]._rendered=false}}if(_parentVnode){vm.$scopedSlots=_parentVnode.data.scopedSlots||emptyObject}//设置父节点。这允许渲染函数访问//占位符节点上的数据。vm.$vnode=_parentVnode//renderself让vnodetry{vnode=render.call(vm._renderProxy,vm.$createElement)}catch(e){handleError(e,vm,`render`)//返回错误renderresult,//或前一个vnode以防止渲染错误导致空白组件/*istanbulignoreelse*/if(process.env.NODE_ENV!=='production'){if(vm.$options.renderError){try{vnode=vm.$options.renderError.call(vm._renderProxy,vm.$createElement,e)}catch(e){handleError(e,vm,`renderError`)vnode=vm._vnode}}else{vnode=vm._vnode}}else{vnode=vm._vnode}}//如果渲染函数出错,则返回空vnodeif(!(vnodeinstanceofVNode)){if(process.env.NODE_ENV!=='production'&&Array.isArray(vnode)){warn('从渲染函数返回多个根节点。渲染函数'+'应该返回单个根节点。',vm)}vnode=createEmptyVNode()}//设置父节点vnode.parent=_parentVnodereturnvnode}该函数内部对不同的逻辑进行了不同的处理,但最终返回的是VNodeVirtualDOM,它使用了一个原生的JS对象来描述一个DOM节点。Vue.prototype._update函数_update是在执行lifecycleMixin(Vue)函数时添加的。在目录src/core/instance/lifecycle.jsVue.prototype._update=function(vnode:VNode,hydrating?:boolean){constvm:Component=thisconstprevEl=vm.$elconstprevVnode=vm._vnodeconstrestoreActiveInstance=setActiveInstance(vm)vm._vnode=vnode//Vue.prototype.__patch__被注入入口点//基于所使用的渲染后端。if(!prevVnode){//初始渲染vm.$el=vm.__patch__(vm.$el,vnode,hydrating,false/*removeOnly*/)}else{//更新vm.$el=vm.__patch__(prevVnode,vnode)}restoreActiveInstance()//更新__vue__引用if(prevEl){prevEl.__vue__=null}if(vm.$el){vm.$el.__vue__=vm}//如果父节点是HOC,更新它的$el也是if(vm.$vnode&&vm.$parent&&vm.$vnode===vm.$parent._vnode){vm.$parent.$el=vm.$el}//更新的钩子是由调度程序调用以确保孩子re//在父母更新的钩子中更新。}通过源码看接受的参数:vnode:VNodehydrating?:boolean接受的第一个参数类型是VNode(用来描述dom节点的虚拟节点),第二个boolean类型是参数是最关键的函数和ssr有关执行vm.$el=vm.__patch__(...)调用vm.__patch__将VNode转换为真正的DOM节点。现在回顾总结一下目前的流程:newVue()运行后,会调用Vue.prototype._init()方法。完成一系列的初始化(给原型添加方法和属性)执行Vue.prototype.$mountvm._render()得到描述当前实例的VNodevm._update(VNode),调用vm.__patch__转换成真正的DOM节点流程图:
