- 组件化可以让我们方便的把页面拆分成多个可重用的组件
- 组件是独立的,系统内可重用,组件之间可以嵌套
- 有了组件可以像搭积木一样开发网页
- 下面我们将从源码的角度来分析 Vue 组件内部如何工作
- 组件实例的创建过程是从上而下
- 组件实例的挂载过程是从下而上
组件声明
全局组件的定义方式:
Vue.component('comp', {template: '<h1>hello</h1>'})
Vue.component() 入口
- 创建组件的构造函数,挂载到 Vue 实例的 vm.options.component.componentName = Ctor
// src\core\global-api\index.js// 注册 Vue.directive()、 Vue.component()、Vue.filter()initAssetRegisters(Vue)// src\core\global-api\assets.jsif (type === 'component' && isPlainObject(definition)) {definition.name = definition.name || iddefinition = this.options._base.extend(definition)}……// 全局注册,存储资源并赋值// this.options['components']['comp'] = Ctorthis.options[type + 's'][id] = definition// src\core\global-api\index.js// this is used to identify the "base" constructor to extend all plainobject// components with in Weex's multi-instance scenarios.Vue.options._base = Vue// src\core\global-api\extend.jsVue.extend()
- 创建组件的构造函数,挂载到 Vue 实例的 vm.options.component.componentName = Ctor
组件构造函数的创建
const Sub = function VueComponent(options) {this._init(options)}Sub.prototype = Object.create(Super.prototype)Sub.prototype.constructor = SubSub.cid = cid++Sub.options = mergeOptions(Super.options,extendOptions)Sub['super'] = Super// For props and computed properties, we define the proxy getters on// the Vue instances at extension time, on the extended prototype. This// avoids Object.defineProperty calls for each instance created.if (Sub.options.props) {initProps(Sub)}if (Sub.options.computed) {initComputed(Sub)}// allow further extension/mixin/plugin usageSub.extend = Super.extendSub.mixin = Super.mixinSub.use = Super.use// create asset registers, so extended classes// can have their private assets too.ASSET_TYPES.forEach(function (type) {Sub[type] = Super[type]})// enable recursive self-lookupif (name) {Sub.options.components[name] = Sub}
组件 VNode 的创建
- 创建根组件,首次 _render() 时,会得到整棵树的 VNode 结构
- 整体流程:new Vue() —> $mount() —> vm._render() —> createElement() —> createComponent()
- 创建组件的 VNode,初始化组件的 hook 钩子函数
// 1. _createElement() 中调用 createComponent()// src\core\vdom\create-element.jselse if ((!data || !data.pre) &&isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {// 查找自定义组件构造函数的声明// 根据 Ctor 创建组件的 VNode// componentvnode = createComponent(Ctor, data, context, children, tag)// 2. createComponent() 中调用创建自定义组件对应的 VNode// src\core\vdom\create-component.jsexport function createComponent(Ctor: Class<Component> | Function | Object | void,data: ?VNodeData,context: Component,children: ?Array<VNode>,tag?: string): VNode | Array<VNode> | void {if (isUndef(Ctor)) {return}……// install component management hooks onto the placeholder node// 安装组件的钩子函数 init/prepatch/insert/destroy// 初始化了组件的 data.hooks 中的钩子函数installComponentHooks(data)// return a placeholder vnodeconst name = Ctor.options.name || tag// 创建自定义组件的 VNode,设置自定义组件的名字// 记录this.componentOptions = componentOptionsconst vnode = new VNode(`vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,data, undefined, undefined, undefined, context,{ Ctor, propsData, listeners, tag, children },asyncFactory)return vnode}// 3. installComponentHooks() 初始化组件的 data.hookfunction installComponentHooks(data: VNodeData) {const hooks = data.hook || (data.hook = {})// 用户可以传递自定义钩子函数// 把用户传入的自定义钩子函数和 componentVNodeHooks 中预定义的钩子函数合并for (let i = 0; i < hooksToMerge.length; i++) {const key = hooksToMerge[i]const existing = hooks[key]const toMerge = componentVNodeHooks[key]if (existing !== toMerge && !(existing && existing._merged)) {hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge}}}// 4. 钩子函数定义的位置(init()钩子中创建组件的实例)// inline hooks to be invoked on component VNodes during patchconst componentVNodeHooks = {init(vnode: VNodeWithData, hydrating: boolean): ?boolean {if (vnode.componentInstance &&!vnode.componentInstance._isDestroyed &&vnode.data.keepAlive) {// kept-alive components, treat as a patchconst mountedNode: any = vnode // work around flowcomponentVNodeHooks.prepatch(mountedNode, mountedNode)} else {// 创建组件实例挂载到 vnode.componentInstanceconst child = vnode.componentInstance =createComponentInstanceForVnode(vnode,activeInstance)// 调用组件对象的 $mount(),把组件挂载到页面child.$mount(hydrating ? vnode.elm : undefined, hydrating)}},prepatch(oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {……},insert(vnode: MountedComponentVNode) {……},destroy(vnode: MountedComponentVNode) {……}}//5 .创建组件实例的位置,由自定义组件的 init() 钩子方法调用export function createComponentInstanceForVnode(vnode: any, // we know it's MountedComponentVNode but flow doesn'tparent: any, // activeInstance in lifecycle state): Component {const options: InternalComponentOptions = {_isComponent: true,_parentVnode: vnode,parent}// check inline-template render functionsconst inlineTemplate = vnode.data.inlineTemplateif (isDef(inlineTemplate)) {options.render = inlineTemplate.renderoptions.staticRenderFns = inlineTemplate.staticRenderFns}// 创建组件实例return new vnode.componentOptions.Ctor(options)}
组件实例的创建和挂载过程
- Vue._update() —> patch() —> createElm() —> createComponent()
// src\core\vdom\patch.js// 1. 创建组件实例,挂载到真实 DOMfunction createComponent(vnode, insertedVnodeQueue, parentElm, refElm) {let i = vnode.dataif (isDef(i)) {const isReactivated = isDef(vnode.componentInstance) && i.keepAliveif (isDef(i = i.hook) && isDef(i = i.init)) {// 调用 init() 方法,创建和挂载组件实例// init() 的过程中创建好了组件的真实 DOM,挂载到了 vnode.elm 上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)) {// 调用钩子函数(VNode的钩子函数初始化属性/事件/样式等,组件的钩子函数)initComponent(vnode, insertedVnodeQueue)// 把组件对应的 DOM 插入到父元素中insert(parentElm, vnode.elm, refElm)if (isTrue(isReactivated)) {reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)}return true}}}// 2. 调用钩子函数,设置局部作用于样式function initComponent(vnode, insertedVnodeQueue) {if (isDef(vnode.data.pendingInsert)) {insertedVnodeQueue.push.apply(insertedVnodeQueue,vnode.data.pendingInsert)vnode.data.pendingInsert = null}vnode.elm = vnode.componentInstance.$elif (isPatchable(vnode)) {// 调用钩子函数invokeCreateHooks(vnode, insertedVnodeQueue)// 设置局部作用于样式setScope(vnode)} else {// empty component root.// skip all element-related modules except for ref (#3455)registerRef(vnode)// make sure to invoke the insert hookinsertedVnodeQueue.push(vnode)}}// 3. 调用钩子函数function invokeCreateHooks(vnode, insertedVnodeQueue) {// 调用 VNode 的钩子函数,初始化属性/样式/事件等for (let i = 0; i < cbs.create.length; ++i) {cbs.create[i](emptyNode, vnode)}i = vnode.data.hook // Reuse variable// 调用组件的钩子函数if (isDef(i)) {if (isDef(i.create)) i.create(emptyNode, vnode)if (isDef(i.insert)) insertedVnodeQueue.push(vnode)}}
