组件
每个组件依赖的 CSS、JavaScript、模板、图片等资源放在一起开发和维护。页面可以有多个组件拼接起来。
模板
main.js
1 | import Vue from 'vue' |
App.vue
1 | <template> |
上一章提到的createElement 方法,它会创建 VNode
1 | function createElement ( |
它最后调用的是_createElement方法
1 | function _createElement ( |
因为上一章是一个普通p标签,所以实例化了一个普通的VNode

而这里这里通过createComponent实例化一个VNode,
vnode = createComponent(Ctor, data, context, children, tag);
1 | function createComponent ( |
在这个过程中关注两个地方
一、安装组件钩子函数
1 | function installComponentHooks (data) { |
定义钩子函数的地方:
1 | var componentVNodeHooks = { |
之后在 VNode 执行 patch 的过程中执行相关的钩子函数
二、实例化VNode
1 | var vnode = new VNode( |
获得vnode之后再调用vm._update,把返回的vnode转换成DOM
1 | Vue.prototype._update = function (vnode, hydrating) { |
上一章有说过走进vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);方法,这里有点稍微不同的地方,从patch方法里调用进createElm方法之后
1 | function createElm ( |

因为是个组件,所以看下createComponent
1 | function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) { |

这里的i就是上面定义钩子函数中的init
1 | init: function init (vnode, hydrating) { |
挂载子组件过程
通过 createComponentInstanceForVnode 创建一个 Vue 的实例,然后调用 child.$mount(hydrating ? vnode.elm : undefined, hydrating);方法挂载子组件
1 | function createComponentInstanceForVnode ( |

断点继续往下走进mountComponent方法
1 | function mountComponent ( |
断点往下到createElm方法里创建了一个占位符,并且通过createChildren方法,创建了一个文本插入到占位符里,之后执行insert方法,但是因为传入的parentElm是undefined,所以直接return了

挂载到根组件
上面的描述的是i(vnode, false /* hydrating */);方法的调用,结束以后
1 | function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) { |
走进去initComponent(vnode, insertedVnodeQueue);方法,这个方法里把子组件实例的$el赋值给了vnode.elm,调用结束之后,调用上面代码中的insert(parentElm, vnode.elm, refElm);,在这个方法里插入到DOM里
1 | function initComponent (vnode, insertedVnodeQueue) { |

- 组件patch过程:createComponent -> 子组件初始化 -> 子组件render -> 子组件patch(如果子组件里还有子组件,就重复这个流程)
- 嵌套组件插入顺序,先子后父,最后挂载到body
配置合并
模板文件
main.js
1 | import Vue from 'vue' |
从初始化开始
1 | Vue.prototype._init = function (options) { |
这里的mergeOptions做数据合并操作,它合并了resolveConstructorOptions(vm.constructor)和options,
resolveConstructorOptions(vm.constructor)的返回值其实是vm.constructor.options
1 | function resolveConstructorOptions (Ctor) { |
那vm.constructor.options是什么,我们知道实例的constructor等于构造函数,也就是vm.constructor.options等于Vue.options
1 | function initGlobalAPI (Vue) { |
上面代码中执行了
1 | Vue.options = Object.create(null); |
Vue.options初始化为一个对象,并且它的__proto__是undefined,之后遍历ASSET_TYPES,为Vue.options添加属性,加完以后是这样的。

后面的extend(Vue.options.components, builtInComponents);,把内置组件添加到了这个属性上。
回到mergeOptions,发现第一个参数上面还多了created方法,这是哪来的(_base就不说了init的时候加的)?

在initGlobalAPI中initMixin$1(Vue);方法,它为Vue构造函数添加了个mixin方法
1 | function initMixin$1 (Vue) { |
也就是说created是我们业务代码中调用Vue.mixin的时候添加的
1 | Vue.mixin({ |
继续看mergeOptions方法
1 | function mergeOptions ( |
mergeOptions 主要功能就是把 parent 和 child 这两个对象根据一些合并策略,合并成一个新对象并返回。比较核心的几步,先递归把 extends 和 mixins 合并到 parent 上,然后遍历 parent,调用 mergeField,然后再遍历 child,如果 key 不在 parent 的自身属性上,则调用 mergeField。mergeField方法对不同的key有不同的处理方式。对于生命周期钩子的处理都是mergeHook方法
1 | var LIFECYCLE_HOOKS = [ |
1 | function mergeHook ( |
如果不存在 childVal ,就返回 parentVal;否则再判断是否存在 parentVal,如果存在就把 childVal 添加到 parentVal 后返回新数组;否则返回 childVal 的数组。所以回到 mergeOptions 函数,一旦 parent 和 child 都定义了相同的钩子函数,那么它们会把 2 个钩子函数合并成一个数组(父的在栈底,子的在栈顶)。
合并完之后返回的对象是这样的:

生命周期
从new Vue开始,到挂载到DOM上,过程中也会运行一些叫做生命周期钩子的函数,给予用户机会在一些特定的场景下添加他们自己的代码。

源码中执行生命周期方法的是通过callHook方法,它在执行的时候,根据传入的字符串 hook,去拿到 vm.$options[hook] 对应的回调函数数组,然后遍历执行,执行的时候把 vm 作为函数执行的上下文。
1 | function callHook (vm, hook) { |
beforeCreated & created
1 | Vue.prototype._init = function (options?: Object) { |
initState 的作用是初始化 props、data、methods、watch、computed 等属性。
beforeCreate:不能获取到 props、data 中定义的值,也不能调用 methods 中定义的函数。
created:如果是需要访问 props、data 等数据的话,可以在这个钩子。
beforeMount & mounted
1 | function mountComponent ( |
在mountComponent的时候调用的声明周期
beforeMount:在执行vm._render()与vm._update()之前
mounted:在执行vm._render()与vm._update()之后,实例插入DOM后,执行这个钩子函数
那么子组件的mounted钩子是在哪里调用
1 | return function patch (oldVnode, vnode, hydrating, removeOnly) { |
从invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);,走进去invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
1 | function invokeInsertHook (vnode, queue, initial) { |
调用走进到queue[i].data.hook.insert(queue[i]);
1 | insert: function insert (vnode) { |
同步渲染的子组件而言,mounted 钩子函数的执行顺序也是先子后父(都是在实例插入到DOM之后调用的)。
beforeUpdate & updated
beforeDestroy & destroyed
activated & deactivated
组件注册
上面说了Vue.mixin是在哪定义的,Vue.component的定义跟Vue.mixin都在initGlobalAPI方法里
1 | function initGlobalAPI (Vue) { |
initAssetRegisters方法
1 | function initAssetRegisters (Vue) { |
所以Vue构造函数一下多了3个静态方法component、directive和filter。
上面有说因为组件不是普通的HTML标签,所以在_createElement方法里走进了createComponent方法
1 | export function _createElement ( |
走进去之前会先判断组件是否存在,通过resolveAsset方法
1 | function resolveAsset ( |
先通过 const assets = options[type] 拿到 assets,然后再尝试拿 assets[id],这里有个顺序,先直接使用 id 拿,如果不存在,则把 id 变成驼峰的形式再拿,如果仍然不存在则在驼峰的基础上把首字母再变成大写的形式再拿,如果仍然拿不到则报错。这样说明了我们在使用 Vue.component(id, definition) 全局注册组件的时候,id 可以是连字符、驼峰或首字母大写的形式。
总结
举个例子吧,看在例子被插入到DOM上,之间发生了什么(目前已知的事情)
1 | import Vue from 'vue' |
App.vue
1 | <template> |
在
initGlobalAPI阶段先在Vue上,初始化Vue.option并且为它添加了初始的字段components、directives和filters,之后Vue.options._base = Vue;,接着为它增加了3个静态方法component、directive和filter(当然还有其他的方法,这里不说了)在业务逻辑中调用
Vue.mixin,做了个mergeOptions操作,之后Vue.options就多了created字段,是个函数数组。

- 之后业务逻辑中调用
Vue.component,这里的this.options就是Vue.options。这里的this.opitons._base.extend做的操作其实就是把这个对象转换成一个继承于Vue的构造函数,最后通过this.options[type + 's'][id] = definition把它挂载到Vue.options.components上

然后进入业务代码的
new Vue调用,先执行初始化_init函数,这一步做合并参数(也就是在这个阶段,生命周期钩子函数合并),初始化参数,并且调用beforeCreate和created钩子函数之后进入
$mount方法进入
mountComponent,调用父组件的beforeMount声明周期钩子进入
vm._render方法(这个方法用于生成vnode)1)走进
createElement方法,发现App不是普通的HTML标签,走进到resolveAsset方法判断

2)发现App是个组件存在,走进createComponent方法(那这个App对象是什么时候被加到context.$options的呢),在第4步,_init函数初始化时候的合并参数mergeOptions方法调用这里

3)installComponentHooks(data);安装组件钩子函数
4)createComponent方法返回值是组件vnode
生成组件
vnode之后调用vm._update方法进入到
patch方法,这个方法做了一些简单的判断,创建了一个空的节点替代了oldVnode的值,然后又拿到了oldVnode.elm的父节点,之后调用进createElm方法进入
createComponent(vnode, insertedVnodeQueue, parentElm, refElm)方法1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
var i = vnode.data;
if (isDef(i)) {
var isReactivated = isDef(vnode.componentInstance) && i.keepAlive;
if (isDef(i = i.hook) && isDef(i = i.init)) {
i(vnode, false /* hydrating */);
}
// 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)) {
initComponent(vnode, insertedVnodeQueue);
insert(parentElm, vnode.elm, refElm);
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
}
return true
}
}
}
1)i(vnode, false /* hydrating */);就是调用了组件的init钩子函数

- 这里的
createComponentInstanceForVnode方法,这里其实是创建了一个Vue的实例

然后调用
$mount挂载子组件,重复进入第5步的步骤(入参会不一样),直到tag是一个普通的HTML标签时,到第10步的createElm里的createComponent(vnode, insertedVnodeQueue, parentElm, refElm)返回值为undefined
指针往下走创建占位符
vnode.elm,就是业务代码中的name-box组件的p标签
接着进入
createChildren方法,遍历子元素,插入到占位符中,就是上面p标签再进入
insert方法,没有parentElm所以这步忽略调用指针返回到
update方法, 里面有个判断如果父级也是组件,就是vm.$parent.$el = vm.$el;
init执行完了,回到这

initComponent方法,vnode.elm=vnode.componentInstance.$el,这个就是组件实例了,等下要插入到DOM里

insert方法,这里没有parentElm,所以直接跳过了
2)返回到最外层的createComponent方法,这是第一次调用createComponent的时候,在这里的insert方法将占位符插入到了DOM中

插入到DOM中后,还有一个
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);,就是执行各组件的mounted钩子函数
之后再执行父组件的
mounted钩子

过程大概就是
new Vue
init初始化
mount
render (这个阶段判断是普通标签还是组件标签,是组件标签就初始化,安装组件钩子函数,最后返回
vnode)- update(进去到patch阶段,这个阶段从最深的地方开始创建占位符,然后把子元素
加进去,一直到整个DOM结构加入#app的父亲下面)
其中各种生命周期先忽略。。
异步组件
改个例子
main.js
1 | import Vue from 'vue' |
创建异步组件的时候,进入resolveAsyncComponent方法
1 | function resolveAsyncComponent ( |
声明resolve

进入once方法,闭包存了一个called的值,只允许传入的fn被调用一次
1 | function once (fn) { |
之后走到var res = factory(resolve, reject);,这个factory方法其实就是,声明组件的第二个参数

之后创建一个异步组件占位符

进入createAsyncPlaceholder方法
1 | function createAsyncPlaceholder ( |
其实就是一个空节点,多了个asyncFactory和asyncMeta

在组件加载的时候,调用到这里

之后进入forceRender方法
1 | function (renderCompleted) { |
进入(owners[i]).$forceUpdate();方法
1 | Vue.prototype.$forceUpdate = function () { |
最后调用的是vm._watcher.update();,之后更新视图