vue源码阅读—03—组件化
第一个问题: mian.js里,new Vue时传递的那个对象参数,对象参数里: render函数里传递普通html标签,和传递一个组件有什么区别?举例:render:h=> h('div',{},['一段文本']) 和render:h=> h(App)
传递一个html标签是不是先在自身上渲染出来? 传递一个组件是不是直接就当作是子组件渲染了?
我觉得,传递一个对象就是传递一个组件,传递一个组件就是要渲染组件节点代替挂载的div,传递一个html标签就是要渲染普通html节点代替挂载的div节点。区别就是用啥去代替挂载的div节点;
还是有区别:
如果是html元素,那么render函数是渲染vnode代替div#app节点;因为是渲染vnode,要遍历自己的子节点,所以在patch流程中要走createChildren();
如果是组件,那么render函数返回的只是一个空的占位符vnode去代替div#app节点;因为是占位符vnode,所以在patch流程中要走createComponent();
第二个问题:
孙子组件Helloworld的patch流程是什么,怎么插入到子组件App的?
子组件app首先走createComponent()这一步,直接创建子组件实例。然后mount子组件,等到子组件patch时,这个时候子组件的render函数最外层应该是html元素所以要走createChildren(),所以回去创建孙子组件;
一、组件化的mount过程
mount过程有两步,即_render()和_update()
Vue.js 另一个核心思想是组件化。所谓组件化,就是把页面拆分成多个组件 (component),每个组件依赖的 CSS、JavaScript、模板、图片等资源放在一起开发和维护。组件是资源独立的,组件在系统内部可复用,组件和组件之间可以嵌套。
我们写的sfc,应该会被vue框架的sfc模块转化为一个js的对象,比如:
{
name:'foo'
date:{}
template:'<div></div>'
method:{}
computed{}
}
组件是一个js对象,里面有render、name、data、methods等属性
后来变成了组件构造函数,继承于Vue构造函数或父构造函数;
最后变成了组件vnode,因为被实例化了Vnode类;
组件对象->组件构造函数-》组件vnode-》组件构造函数实例
整个patch的过程,可以理解为是一个深度遍历的过程,在建立一棵vnode树;
子组件实例vm有三个属性
- vm.options.partent:指的是vue实例
- vm.options._parentElm指的是body标签
- vm.options._parentVnode指的是占位符vnode
组件的mount过程:
我们以这样举例: main.js: new Vue({ ... render: h => h(App); }) App.js: { name:'App', data(){}, methods:{}, render: h => h('div', {}, ['一段文本','第二段文本']), }
render:
- 调用vue实例的mount方法,通过h函数,将生成根组件为一个占位符vnode,当然也可以叫组件vnode;(如果h写的不一样,则可能是渲染vnode,可能是占位符vnode,可能是注释vnode,可能是文本vnode;)
update:
- 发现传入的是一个组件App,然后createComponet,最后创建好一个组件实例,结果又需要重新调用子组件实例mount方法,结果发现子组件的render函数里传入的不是孙子组件而是普通的孙子html元素div,故生成孙子渲染vnode。然后patch孙子vnode,然后挂载子vnode,然后挂载父vnode,但是父vnde啥也没有只是一个占位符vnode所以会被替换,最后删除div#app, patch完成;
渲染vnode和占位符vnode
<template> <div class="我是渲染vnode" > <img src="渲染vnode节点">
<!-- 注释vnode节点-->
{{文本vnode节点}} <HellogWorld>我是组件的占位符vnode,也可以叫组件vnode;</HellogWorld> </div> </template>
那么在render函数中,渲染vnode和占位符vnode是什么样的?
根组件的render函数如果这样写:render:h=> h(App)
那么根组件是一个占位符vnode;
根组件的render函数如果这样写:render: h=> h('div',{},['一段文本',App])
那么根组件是一个渲染vnode;
根组件的render函数如果这样写:render: h=> h()
那么根组件是一个注释vnode;
根组件的render函数如果这样写:render: h=> h(’我是一段文本内容‘)
那么根组件是一个文本vnode;
组件对象,组件构造函数、组件实例、组件vnode(组件vnode就是render过程中通过new Vnode生成的vnode,一般来说都是渲染vnode,因为template的div里会包含很多子vnode,比如上文代码;
update过程中的patch,详细说说
patch.js文件的patch函数中,会调用createElm函数,那它的作用是:
首先我们要知道,到了mount的update流程,所有的render函数都已经变成了vnode节点,只不过是不同的vnode节点,可能是渲染、组件、注释、文本vnode,
那么对于不同的vnode节点,我们要把它patch到真实dom上,有不同的流程;
如果是render:h=》h(App)先生成占位符vnode;然后子组件初始化;然后子组件mount的render过程生成渲染vnode,然后子组件mount的update即patch过程中,因为子组件是渲染vnode,所以走createChildren, 所以如果子组件的渲染vnode里面还有组件的话,就继续调用craetComponent;但是因为组件不可能无限循环的嵌套下去,总会有一个都是普通节点;所以会最终生成普通渲染节点或注释节点或文本节点,
然后把普通渲染节点或注释节点或文本节点都插入到真实dom中;
然后子节点插入完了,插入真实dom节点;
最后删除div#app, 调用callhook('vm',mounted);
vm.$vnode是占位符vnode;
vm._vnode是组件的渲染vnode;
activeInstanceh是当前激活的vm实例;
插入顺序是先子后父;
是这样的,子节点会先创建vnode,因为执行父节点的render函数时,h函数会先执行子节点的,并且h函数生先调用normalizeChildren()去把chrildren拍平顺便把里面的元素都变成vnode;
子节点也会先把patch,因为本身要patch到真实dom上时会调用createChildren方法;等到createChildren好之后才会调用insert方法把自己插入到真实dom;
oldVnode就是div#app
parentVnode就是body标签
preVnode就是上一次的Vnode用于做diff算法比对的;
以这个举例:
<template> <div class="我是渲染vnode" > <img src="普通vnode节点"> {{普通vnode节点}} <HellogWorld>我是组件的占位符vnode</HellogWorld> </div> </template>
那么普通节点和组件节点的mount到底有社么不同?
1.render中没有不同,
- 都是生成一个vnode,只不过普通vnode的tag是原生html标签,一般来说这个vnode是普通vnode或者渲染vnode;;
- 都是生成一个vnode,组件vnode的tag是组件名字,而且一般来说这个vnode是占位符vnode;然后生成一个子组件构造函数;
2.update中即patch中有很大不同:
- 普通节点的ptach在执行createElm()是生成一个正常的dom节点
- 组件的patch在执行createElem()是生成一个子组件构造函数的实例,然后继续调用这个实例的mount方法。
组件的patch在执行createElem()时就不一样了,会进入createComponent(vnode, insertedVnodeQueue, parentElm, refElm)方法,然后再调用componentVnodeHooks的init方法,然后又会调用createComponentInstanceForVnode方法,然后又会调用vnode.componentOptions.Ctor(options)即new一个子组件实例;
然后new这个子组件构造函数为子组件实例添加了很多配置,后面又接着调用child.$mount()方法其实就是生成这个子组件渲染vnode的过程;然后子组件的渲染vnode准备patch到真实dom上时,会调用createChildren()就是对每一个子节点调用createElm方法,如果是普通vnode则生成一个普通dom如果是组件vnode则继续重复进入createComponent的操作生成组件实例;
1.2activeInstance的用处
子组件实例可以通过该vm.$parent访问到父组件实例,如果实现的?
通过activeInstance实现的;
在子组件实例调用this_init()时,会调用initLiftCycle方法,然后会有一个
vm.$parent = parent ( = InternalComponentOptions.parent = acitveINstacne) 的赋值过程;
然后,我们可以构建一个vm实例的树状结构;注意,还有一个vnode树;
二、组件的合并配置
Vue.mixin({ created() { console.log("parent created"); }, }); const App = { name: "App", data() { return { name: "i am app.vue", }; }, render(h) { return h('div',{id:'div#InAppVue'},this.name) }, created() { console.log("appvue created"); }, mounted() { console.log("appvue mounted"); }, }; new Vue({ el: "#app", data: { msg: "hello vue", }, render: (h) => h(App), });
我们以这个为例,
1.首先,Vue.mixin会调用mergeOptions方法;将created()添加到Vue.optons.created属性上去;
2.其次,在new Vue的过程中,会调用this._init(),然会执行这段代码,所以,Vue.options和我们自定义的options都会合并并都赋值给vm.$options;这个vm是Vue实例;
else { // 场景2:外部场景调用时,即我们在new Vue时做的options合并; vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), //这个函数resolveCos其实返回的是Vue.options;Vue.options在/global-api/index.js里定义的; options || {}, vm ) }
3. 然后,在_render()过程中,我们创建了App组件vnode,并把App组件对象转化为App组件构造函数;在把App组件对象转化为App组件构造函数的过程中,sub构造函数会进行一个mergeOption,
这个时候Vue.options和子组件App自定义的options都做了合并,并赋值给Sub.options;
Sub.options = mergeOptions(
Super.options,
extendOptions
)
4.然后再_update()过程中,创建了子组件的实例,然后调用了this_init方法,然后执行这段代码,
// 场景1.注册子组件时,options的合并 if (options && options._isComponent) { // optimize internal component instantiation //优化内部组件实例化,因为动态选项合并非常慢,而且内部组件选项都不需要特殊处理。 // since dynamic options merging is pretty slow, and none of the // internal component options needs special treatment. //注意这options是什么?这个options是/vdom/create-component.js文件里的createComponentInstanceForVnode方法定义的options //子组件自己定义的options已经在定义子组件构造函数时,和Vue.options做了合并了,并且赋值给了sub.options; 详情见src\core\global-api\extend.js initInternalComponent(vm, options)
然后执行这个函数
//options参数可不是子组件对象,而是createComponent.js文件createComponentInstanceForVnode函数里定义的options局部变量; export function initInternalComponent (vm: Component, options: InternalComponentOptions) {
// doing this because it's faster than dynamic enumeration. //我们在实例化子组件时需要传入父组件的vnode及父组件的实例 const parentVnode = options._parentVnode opts.parent = options.parent opts._parentVnode = parentVnode opts._parentElm = options._parentElm opts._refElm = options._refElm const vnodeComponentOptions = parentVnode.componentOptions opts.propsData = vnodeComponentOptions.propsData opts._parentListeners = vnodeComponentOptions.listeners opts._renderChildren = vnodeComponentOptions.children opts._componentTag = vnodeComponentOptions.tag if (options.render) { opts.render = options.render opts.staticRenderFns = options.staticRenderFns } }
然后我们就发现,所有的options也被赋值给了vm.$options,只不过这个sub是Sub构造函数实例即App构造函数实例;
5.至此,所有配置都被合并了。
- Vue.options会合并mixin里的东西,也有一些vue官方定义的配置;
- vm.$options会合并Vue.options和自定义的的配置;
- Sub.options会合并Vue.options和自定义的配置;
- sub.$options会合并Sub.options和 createComponent.js文件createComponentInstanceForVnode函数里定义的options配置;
三、组件的生命周期钩子函数
1.执行顺序
- beforeMounted先父后子:原因很简单,父调用完后生成父vnode,然后再父组件patch过程中才会新建sub实例,才会重新走一遍sub_init方法,才会sub.$mount(),然后才会调用beforeMOunted钩子;
- mounted先子后父:因为先插入子元素,子元素插入完了才插入父元素;
- beforeDestroy先父后子
- destroed先子后父
2.执行时机
- beforeMount()在确保有render函数时,在生成vnode前,执行; -----------instance/lifecycle.js/mountComponent();
- mounted()当元素插入到真实dom上时执行
- beforeUpdate(): 在flushSchedualerQueue后,会触发watcher的重新渲染,即触发watcher的vm._update(vm._render()),然后vm._update()方法的第一步就是调用callHook(vm, 'beforeUpdate');
- updated(): 在flushSChedualerQueue执行完后,即所有的watcher都重新渲染完后,会调用callUpdatedHooks(updatedQueue),然后调用每一个vm的updated钩子函数;
- destory()不管是用v-if还是点击切换组件,都会触发flag的变化把,那么就会触发渲染vnode的重新渲染,那么就会repatch,那么在diff的时候就会触发removeNodes,然后就触发invokeVnodeDestorys;然后会调用vm.$destory(), 然后会调用callHook('beforeDestory') callHook('destory');
data.hook.create是啥?
cbs是啥?
modules里的一些方法,和和direactive自定义指令有关;
destory的执行时机?
不管是用v-if还是点击切换组件,都会触发flag的变化把,那么就会触发渲染vnode的重新渲染,那么就会repatch,那么在diff的时候就会触发removeNodes,然后就触发invokeVnodeDestorys;
3.一个.vue文件的执行流程
在此,也搞清楚了一个.vue文件或者jsx文件的执行流程了。
- created()
- beforeMount()
- 执行render函数部分;(如果是.vue文件就是准备执行由template模板转化的render函数)
- 执行mounted()
我们在配置项里比如data,methods、created()、render()这些函数中用的this,都是指向当前组件实例的,因为都是这样被调用的。
还记得render()是怎么被调用的吗? render.call(vm._renderProxy,vm.$createElement); 而vm._renderProxy指的也是本身;
至于为什么我们在methods的函数中,可以通过this.name就去访问到this.options.data.name,这个是因为做了proxy;
四、组件注册
通过Vue.component全局注册组件时,这个方法会将组件挂载到Vue.options.components[key]上,并且将每一个组件对象转化为组件构造函数。
因为不管是vm实例,还是子组件实例,都会合并mergeOptions时合并Vue.options,所以,每一个实例都可以访问到全局注册的组件;
具体怎么访问的呢?
就是在mount过程的—render过程中,由于要创建vnode,走tag=string这一步,然后通过resolveAsset方法返回子组件构造函数;
五、异步组件的加载流程
我们为了减少首屏加载时的js包体积,可以使用异步组件的方式加载组件。不需要在首屏刚加载时就把所有的js组件都加载上来,可以是点击某个按钮后再加载某个组件也可以是等待一段时间后再去加载首屏中不重要的组件或首屏后面的组件,这样的好处是可以让首屏加载的速度更快,免得一个index页要加载十多秒,严重影响用户体验;
Vue.component('App',function(resolve,reject){ setTimeout(() => { resolve(App) }, 1000); }); new Vue({ el: "#app", data: { msg: "hello vue", }, //传递一个已经全局注册过的组件 render: (h)=> h('App') });
以这个举例,在延迟一秒后加载App组件;
首先,因为是注册的组件,所以肯定还是走这个逻辑,然后运行后,ctor不是一个构造函数,而是这个工厂函数
function(resolve,reject){
setTimeout(() => {
resolve(App)
}, 1000);
});
然后执行createComponent,因为是异步组件加载,所以走这个逻辑
然后执行res = factory(resolve,reject);
本质上也就是执行我们自己写的工厂函数
function(resolve,reject){ setTimeout(() => { resolve(App) }, 1000); });
但是有一点注意,执行自己写的工厂函数时,settimeout是在延时结束后被添加到宏任务队列了是要在nexttick执行的,所以我们这里不会执行settimeout里面的代码。
那我们继续执行后续代码,即return一个undefined;
然后执行createAsyncPlaceholder,返回一个注释vnode;
后面的代码,就是我们正常patch的代码,将注释vnode变成真实的注释dom,然后挂载到div#app上;
但是注意,一秒之后(就是settimeout设置的延时之后),也就是在nexttick中,settimeout里面的代码开始执行了,这个时候就会执行到resolve(APP),也就是执行
也就是执行下面的代码,然后factor.resolved = 子组件构造函数;
然后带哦用forceRender,也就是调用每个vm.$forceUpdate()方法,然后每个实例重新进行mount过程,
也就是要重新执行一边vm._render()和vm._update();
那么在创造vnode的过程中,我们发现这个if条件为真可以直接返回一个子组件构造函数了;
后面的update过程都是一个样的patch了,不再赘叙;
总结: