vue2.x源码解析六——组件化--4.实例解析组件的整个映射过程
1.准备工作
1.加入断点
我们利用断点的方式,一步一步分析,,我们采用的是Runtime+Compiler版本的vue.js,所以我们将debugger
插入组件DOM的时候会走createComponent函数
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
...
if (isDef(vnode.componentInstance)) {
debugger
...
}
}
path的时候
return function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {
debugger
}
1.2 例子
我们的例子为
目录:
|-main.js ——– 写Vue实例 ( 用A.vue代替)
|-app.vue ——– 组件 (用B.vue代替)
|-HelloWorld.vue —— 组件 (用C.vue代替)
main.js
使用app.vue组件
import Vue from 'vue'
import App from './App'
import router from './router'
Vue.config.productionTip = false
/* eslint-disable no-new */
new Vue({
el: '#app',
router,
components: { App },
template: '<App/>'
})
app.vue
HelloWorld.vue会插入到app.vue中
<template>
<div id="app">
<img src="./assets/logo.png">
<hello></hello>
</div>
</template>
<script>
import hello from './components/HelloWorld'
export default {
name: 'App',
components: {
hello
}
}
</script>
HelloWorld.vue
<div class="hello">
<h1>{{ msg }}</h1>
<h2>Essential Links</h2>
<h2>Ecosystem</h2>
<ul>
<li>
</li>
<li>
</li>
</ul>
</div>
export default {
name: 'HelloWorld',
data () {
return {
msg: 'Welcome to Your Vue.js App'
}
}
}
注释:
占位符节点:
渲染B组件的时候,B组件中引入了组件,那么 <hello></hello>
就是占位符节点,同理A使用B组件的时候也会有占位符节点
渲染VNode
B组件的外层div, <div id="app">
,
因为它有子节点,也就是children,他的children都会保留,所以拿到渲染VNode也就是根VNode就可以了,可以用它去遍历children,拿到这个子节点VNode树
2 过程
1.进入函数path
return function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm){
}
参数:
oldVnode :div#app 原生的DOM节点,就是我们vue实例话的时候的 div id=“app”
vnode : vue-component-4-App 就是我们的组件app.vue
2
因为是最开始的path,所以 isRealElement 设置为true
3
oldVnode = emptyNodeAt(oldVnode);
将oldVnode 转化为VNode
4.第一次执行createElm做挂载
也就是对app.vue进行挂载
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
if (isDef(vnode.elm) && isDef(ownerArray)) {
vnode = ownerArray[index] = cloneVNode(vnode);
}
vnode.isRootInsert = !nested; // for transition enter check
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
会执行createComponent这个方法
5.
执行createComponent这个方法
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
var i = vnode.data;
if (isDef(i)) {
var isReactivated = isDef(vnode.componentInstance) && i.keepAlive;
// 这里会为true,执行hook中的init方法
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false /* hydrating */, parentElm, refElm);
}
// after calling the init hook, if the vnode is a child component
// it should've created a child instance and mounted it. the child
// component also has set the placeholder vnode's elm.
// in that case we can just return the element and be done.
if (isDef(vnode.componentInstance)) {
debugger
initComponent(vnode, insertedVnodeQueue);
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
}
return true
}
}
}
vnode是app.vue组件,组件中是有data和hook钩子的,所以会执行执行hook中的init方法
6.
componentVNodeHooks
const componentVNodeHooks = {
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
if (
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
// kept-alive components, treat as a patch
const mountedNode: any = vnode // work around flow
componentVNodeHooks.prepatch(mountedNode, mountedNode)
} else {
//child是一个vnode实例
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode, // 当前组件的vnode
activeInstance // 当前的vue实例 就是div#app,也就是当前组件的父vue实例
)
//调用 $mount 方法挂载子组件
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
},
prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
},
insert (vnode: MountedComponentVNode) {
},
destroy (vnode: MountedComponentVNode) {
}
}
这是会自动给组件添加的hook,执行的就是这个hook种的init方法,会走else,就会走createComponentInstanceForVnode方法
7.
进入createComponentInstanceForVnode方法
function createComponentInstanceForVnode (
vnode, //
parent, //
parentElm,
refElm
) {
// 定义options
var options = {
_isComponent: true,
parent: parent, // vue实例
_parentVnode: vnode, // 这里就是站位父VNode,也就是app.vue的占位符
_parentElm: parentElm || null, // vue实例的外层元素,就是div#app的外层,这里是body
_refElm: refElm || null
};
// check inline-template render functions
var inlineTemplate = vnode.data.inlineTemplate;
if (isDef(inlineTemplate)) {
options.render = inlineTemplate.render;
options.staticRenderFns = inlineTemplate.staticRenderFns;
}
// 最后就会走到这个构造器
return new vnode.componentOptions.Ctor(options)
}
最后就会走到构造器
8.
上面进入到了子构造器
return new vnode.componentOptions.Ctor(options)
然后就会执行构造函数
var Sub = function VueComponent (options) {
this._init(options);
};
Sub是继承于Vue,所以会有跟Vue一样的原型方法,就会走_init函数
9
Vue.prototype._init = function (options) {
// 因为是组件,所以合并options会走这里
if (options && options._isComponent) {
initInternalComponent(vm, options);
} else {
}
// 初始化生命周期
initLifecycle(vm);
//vm.$options就是B组件,B组件是没有el的,所以不会执行,回跳出init函数,去执行$mount
if (vm.$options.el) {
vm.$mount(vm.$options.el);
}
}
1.合并options
因为是组件,所以合并options会initInternalComponent函数
function initInternalComponent (vm, options) {
// 返回一个空对象
var opts = vm.$options = Object.create(vm.constructor.options);
var parentVnode = options._parentVnode;
opts.parent = options.parent; // Vue实例
opts._parentVnode = parentVnode; // 占位符VNode
opts._parentElm = options._parentElm;
opts._refElm = options._refElm;
// 将占位符VNode的一些属性赋值给opts
var 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;
}
}
2. 初始化生命周期
initLifecycle(vm);
建立组件实例和new Vue的实例的父子关系
function initLifecycle (vm) {
var options = vm.$options; // B组件实例
// locate first non-abstract parent
var parent = options.parent; // new Vue的实例
if (parent && !options.abstract) {
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent;
}
// 向 new Vue的实例中push组件实例
parent.$children.push(vm);
}
// 组件的$parent指向new Vue的实例
vm.$parent = parent;
vm.$root = parent ? parent.$root : vm;
。。。
}
10
会跳回到 6节 执行
//调用 $mount 方法挂载子组件
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
就会执行$mount方法
var mount = Vue.prototype.$mount;
Vue.prototype.$mount = function (
el,
hydrating
) {
// 挂载的真实DOM
el = el && query(el);
if (el === document.body || el === document.documentElement) {
process.env.NODE_ENV !== 'production' && warn(
"Do not mount Vue to <html> or <body> - mount to normal elements instead."
);
return this
}
// 组件实例
var options = this.$options;
// 这里的render函数是有的,因为组件会被vue-loader转化为对象,对象会有render渲染函数
if (!options.render) {
}
//最终执行的是Vue原生的mount
return mount.call(this, el, hydrating)
};
因为采用的是Runtime+Compiler版本的vue.js,所以没有直接走原生的mount方法,但是最终执行的是Vue原生的mount
11.
Vue.prototype.$mount = function (
el,
hydrating
) {
el = el && inBrowser ? query(el) : undefined;
return mountComponent(this, el, hydrating)
};
发现他执行的是mountComponent方法
12
function mountComponent (
vm,
el,
hydrating
) {
vm.$el = el;
...
var updateComponent;
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
...
} else {
updateComponent = function () {
// vm._update,vm._render()
vm._update(vm._render(), hydrating);
};
}
// 去监控执行 vm._update
new Watcher(vm, updateComponent, noop, null, true /* isRenderWatcher */);
hydrating = false;
...
return vm
}
会生成渲染watcher,去监控执行 vm._update,也就是path,我们先看一下vm._render()
13
上面的vm._render()
将子元素都生成VNode
Vue.prototype._render = function () {
vm.$vnode = _parentVnode; // 将占位符vnode赋值给$vnode
// 调用render去生成渲染vnode,就是app组件的外层div, `<div id="app">`
vnode = render.call(vm._renderProxy, vm.$createElement);
。
。
。
// 将渲染vnode指向 占位符vnode节点
vnode.parent = _parentVnode;
// 返回渲染vnode
return vnode
}
渲染vnode
app.vue组件的外层div, <div id="app">
,
因为它有子节点,也就是children,他的children都会保留,所以拿到渲染VNode也就是根VNode就可以了,可以用它去遍历children,拿到这个子节点VNode树
返回 返回渲染vnode后, vm._update就会去调用渲染vnode
14
当前为B组件实例实例化的过程,此时的activeInstance自然是最外层的vue实例,但是我们会将B组件实例赋值给activeInstance
Vue.prototype._update = function (vnode, hydrating) {
//保存activeInstance,为Vue,因为我们是B组件的实例化过程,activeInstance是Vue的实例化过程
var prevActiveInstance = activeInstance;
// activeInstance 保存当前实例,是B组件实例
activeInstance = vm;
// B组件实例的_vnode去保留渲染vnode
vm._vnode = vnode;
// 这是就回去执行子组件的patch,也就是B组件的初始化,将行子组件的patch结果赋值给B组件实例.$el
if (!prevVnode) {
vm.$el = vm.__patch__(
vm.$el, vnode, hydrating, false /* removeOnly */,
vm.$options._parentElm,
vm.$options._refElm
);
vm.$options._parentElm = vm.$options._refElm = null;
} else {
// 渲染B组件的内容,再次调用__patch__方法
vm.$el = vm.__patch__(prevVnode, vnode);
}
}
当我们patch完最外层,就会返回B组件的占位符vnode,占位符vnod执行整个B组件初始化过程中才会去渲染子组件
接着执行patch方法
15
对B组件的子组件进行patch
return function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm){
}
参数:
oldVnode :undefined,因为B组件没有绑定元素呢
vnode : 就是我们的B组件(app.vue)的渲染vnode,里面包含着子VNode
接着就会执行createElm方法,进行挂载
if (isUndef(oldVnode)) {
// empty mount (likely as component), create new root element
isInitialPatch = true;
createElm(vnode, insertedVnodeQueue, parentElm, refElm);
} else {
}
16
function createElm (
vnode, //B组件(app.vue)的渲染vnode
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
if (isDef(vnode.elm) && isDef(ownerArray)) {
vnode = ownerArray[index] = cloneVNode(vnode);
}
vnode.isRootInsert = !nested; // for transition enter check
// 这个时候vnode是渲染vnode,也就是app.vue最外层的div#app,并不是组件,所以不会走这里
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return
}
// 拿到app.vue的data
var data = vnode.data;
// 拿到app.vue的所有子节点,第三个子节点是我们helloWorld.vue组件
var children = vnode.children;
var tag = vnode.tag;
if (isDef(tag)) {
if (process.env.NODE_ENV !== 'production') {
if (data && data.pre) {
creatingElmInVPre++;
}
if (isUnknownElement$$1(vnode, creatingElmInVPre)) {
warn(
'Unknown custom element: <' + tag + '> - did you ' +
'register the component correctly? For recursive components, ' +
'make sure to provide the "name" option.',
vnode.context
);
}
}
// 给渲染VNode.elm创建一个DOM,
vnode.elm = vnode.ns
? nodeOps.createElementNS(vnode.ns, tag)
: nodeOps.createElement(tag, vnode);
setScope(vnode);
/* istanbul ignore if */
// 执行 createChildren,子组件插入到app.vue的占位符中
{
// createChildren的时候回递归的执行createElm方法,遇到我们的helloWorld组件的时候就会再次执行createComponent这个方法
createChildren(vnode, children, insertedVnodeQueue);
if (isDef(data)) {
invokeCreateHooks(vnode, insertedVnodeQueue);
}
insert(parentElm, vnode.elm, refElm);
}
if (process.env.NODE_ENV !== 'production' && data && data.pre) {
creatingElmInVPre--;
}
} else if (isTrue(vnode.isComment)) {
vnode.elm = nodeOps.createComment(vnode.text);
insert(parentElm, vnode.elm, refElm);
} else {
vnode.elm = nodeOps.createTextNode(vnode.text);
insert(parentElm, vnode.elm, refElm);
}
}
17
子节点插入父节点,父节点插入爷爷节点
总结
patch的总体过程:
createComponents (返回为true)–> 子组件初始化 (_init –>createComponentInstanceForVnode –>initLifecycle初始化 –> mount挂载)–> 子组件render(生成渲染vnode) –> 子组件patch –> 遍历子组件的渲染VNode –> 如果发现子组件中还有组件就递归的调用 createComponents
activeInstance为当前**的vm实例,会作为子组件的parent传入,因为如果有多层组件的套用,是需要深层遍历的
vm.$vnode是组件的占位符节点
vm._vnode是渲染VNode
嵌套组件的插入顺序是先子后父。
A.vue 调用 B.vue 调用 C.vue
判断 A中有B组件 –> 生成B组件占位VNode –> 生成B组件渲染VNode –> 遍历B组件渲染VNode –> 发现组件C –> 生成C组件占位VNode –> 生成C组件渲染VNode –> 遍历C组件渲染VNode –> 将生成的DOM节点插入 C组件 —> 将C组件插入B组件 –> B组件插入A组件
上一篇: Django调试利器django-debug-toolbar
下一篇: 最快最简单的排序——桶排序