组件
每个组件依赖的 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();
,之后更新视图