vue组件化过程——Vue源码解析
component的过程很有意思的过程
vue组件化过程——Vue源码解析
component的过程很有意思的过程
大家都知道,使用Vue组件化是整个引擎的核心内容。
什么是组件化呢?
就是把页面拆分成多个组件 (component),每个组件依赖的 CSS、JavaScript、模板、图片等资源放在一起开发和维护。组件是资源独立的,组件在系统内部可复用,组件和组件之间可以嵌套。
我们在用 Vue.js 开发实际项目的时候,就是像搭积木一样,编写一堆组件拼装生成页面。在 Vue.js 的官网中,也是花了大篇幅来介绍什么是组件,如何编写组件以及组件拥有的属性和特性。
我们也通过之前的分析指导,createElement
是个很重要的方法返回的是Vnode的结构,其中入参更多是原生的标签,这次入参是组件,咱们一起探索一下:
createElement
最终会调用_createElement
方法,其中关于传入的tag判断中如果是普通的html
节点标签,比如p
标签,则最终会实例化一个VNode
的节点,或者是个组建的话就创建个 组件转化为vnode的节点
if (typeof tag === 'string') { let Ctor ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag) if (config.isReservedTag(tag)) { // platform built-in elements vnode = new VNode( config.parsePlatformTagName(tag), data, children, undefined, undefined, context ) } else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) { // component vnode = createComponent(Ctor, data, context, children, tag) } else { // unknown or unlisted namespaced elements // check at runtime because it may get assigned a namespace when its // parent normalizes children vnode = new VNode( tag, data, children, undefined, undefined, context ) } } else { // direct component options / constructor vnode = createComponent(tag, data, context, children) }
代码 1.1
这次我们通过初始化Vue的时候App.vue对象的代码传入的是如下配置:
<template> <div id="app"> <img alt="Vue logo" src="./assets/logo.png"> <HelloWorld msg="Welcome to Your Vue.js App"/> </div> </template> <script> import HelloWorld from './components/HelloWorld.vue' export default { name: 'app', components: { HelloWorld } } </script>
代码 1.2
如上面的代码结构,其实是个Component
类型,那么在代码1.1
中就会走到else的方法中createComponent
方法。最后还是生成了vnode
。那么这个createComponent
就是 本次分析的重点。路径在src/core/vdom/create-component.js
:
function createComponent ( Ctor: Class<Component> | Function | Object | void, data: ?VNodeData,hooksToMerge context: Component, children: ?Array<VNode>, tag?: string ): VNode | void { if (isUndef(Ctor)) { return } const baseCtor = context.$options._base // plain options object: turn it into a constructor if (isObject(Ctor)) { Ctor = baseCtor.extend(Ctor) } // if at this stage it's not a constructor or an async component factory, // reject. if (typeof Ctor !== 'function') { if (process.env.NODE_ENV !== 'production') { warn(`Invalid Component definition: ${String(Ctor)}`, context) } return } // async component let asyncFactory if (isUndef(Ctor.cid)) { asyncFactory = Ctor Ctor = resolveAsyncComponent(asyncFactory, baseCtor, context) if (Ctor === undefined) { // return a placeholder node for async component, which is rendered // as a comment node but preserves all the raw information for the node. // the information will be used for async server-rendering and hydration. return createAsyncPlaceholder( asyncFactory, data, context, children, tag ) } } data = data || {} // resolve constructor options in case global mixins are applied after // component constructor creation resolveConstructorOptions(Ctor) // transform component v-model data into props & events if (isDef(data.model)) { transformModel(Ctor.options, data) } // extract props const propsData = extractPropsFromVNodeData(data, Ctor, tag) // functional component if (isTrue(Ctor.options.functional)) { return createFunctionalComponent(Ctor, propsData, data, context, children) } // extract listeners, since these needs to be treated as // child component listeners instead of DOM listeners const listeners = data.on // replace with listeners with .native modifier // so it gets processed during parent component patch. data.on = data.nativeOn if (isTrue(Ctor.options.abstract)) { // abstract components do not keep anything // other than props & listeners & slot // work around flow const slot = data.slot data = {} if (slot) { data.slot = slot } } // merge component management hooks onto the placeholder node mergeHooks(data) // return a placeholder vnode const name = Ctor.options.name || tag const vnode = new VNode( `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`, data, undefined, undefined, undefined, context, { Ctor, propsData, listeners, tag, children }, asyncFactory ) return vnode }
代码 1.3
呃,好复杂,简直发狂~~,不过虽然逻辑复杂一些,我们只是看重点的主要流程:
根据代码1.3
的第一部分代码如下:
const baseCtor = context.$options._base // plain options object: turn it into a constructor if (isObject(Ctor)) { Ctor = baseCtor.extend(Ctor) }
代码 1.4
再根据代码1.2
的业务逻辑梳理过程中,看一看到,export
导出的是一个对象,所以创建组件的过程中会执行到Ctor = baseCtor.extend(Ctor)
, 在这里baseCtor
实际上就是Vue,在之前初始化的过程中已经可以查看到Vue.options._base = Vue
在src/core/global-api/index.js
中initGlobalAPI
函数中体现。
还有关于context.$options
其实就是前面的vm.$options
进行了合并后的结果
vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm )
代码 1.5
简单了解了baseCtor
指向Vue后,还有extend
的由来src/core/global-api/extend.js
/** * Each instance constructor, including Vue, has a unique * cid. This enables us to create wrapped "child * constructors" for prototypal inheritance and cache them. */ Vue.cid = 0 let cid = 1 /** * Class inheritance */ Vue.extend = function (extendOptions: Object): Function { extendOptions = extendOptions || {} const Super = this const SuperId = Super.cid const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {}) if (cachedCtors[SuperId]) { return cachedCtors[SuperId] } const name = extendOptions.name || Super.options.name if (process.env.NODE_ENV !== 'production') { if (!/^[a-zA-Z][\w-]*$/.test(name)) { warn( 'Invalid component name: "' + name + '". Component names ' + 'can only contain alphanumeric characters and the hyphen, ' + 'and must start with a letter.' ) } } const Sub = function VueComponent (options) { this._init(options) } Sub.prototype = Object.create(Super.prototype) Sub.prototype.constructor = Sub Sub.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 usage Sub.extend = Super.extend Sub.mixin = Super.mixin Sub.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-lookup if (name) { Sub.options.components[name] = Sub } // keep a reference to the super options at extension time. // later at instantiation we can check if Super's options have // been updated. Sub.superOptions = Super.options Sub.extendOptions = extendOptions Sub.sealedOptions = extend({}, Sub.options) // cache constructor cachedCtors[SuperId] = Sub return Sub }
代码 1.6
看到了Sub创建的流程了吧
const Sub = function VueComponent (options) { this._init(options) }
代码 1.7
很有意思的代码初始化逻辑。
再继续往下看,基本就是继承集合的体现,这个挺有意思的方法,值得大家一起去学习,其实就是原型继承
、构造继承
、深浅复制继承
等组合方式去实现的继承。目的是构造一个Vue的子类。当然还要记得将 子类继承对象指向自身Sub.prototype.constructor = Sub
// merge component management hooks onto the placeholder node mergeHooks(data)
代码 1.8
大家都知道Vue在VNode的patch过程中暴露了各种钩子函数,组件化的过程中也是一样的
function mergeHooks (data: VNodeData) { if (!data.hook) { data.hook = {} } for (let i = 0; i < .length; i++) { const key = hooksToMerge[i] const fromParent = data.hook[key] const ours = componentVNodeHooks[key] data.hook[key] = fromParent ? mergeHook(ours, fromParent) : ours } }
代码 1.9
const componentVNodeHooks = { init ( vnode: VNodeWithData, hydrating: boolean, parentElm: ?Node, refElm: ?Node ): ?boolean { if (!vnode.componentInstance || vnode.componentInstance._isDestroyed) { const child = vnode.componentInstance = createComponentInstanceForVnode( vnode, activeInstance, parentElm, refElm ) child.$mount(hydrating ? vnode.elm : undefined, hydrating) } else if (vnode.data.keepAlive) { // kept-alive components, treat as a patch const mountedNode: any = vnode // work around flow componentVNodeHooks.prepatch(mountedNode, mountedNode) } }, prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) { const options = vnode.componentOptions const child = vnode.componentInstance = oldVnode.componentInstance updateChildComponent( child, options.propsData, // updated props options.listeners, // updated listeners vnode, // new parent vnode options.children // new children ) }, insert (vnode: MountedComponentVNode) { const { context, componentInstance } = vnode if (!componentInstance._isMounted) { componentInstance._isMounted = true callHook(componentInstance, 'mounted') } if (vnode.data.keepAlive) { if (context._isMounted) { // vue-router#1212 // During updates, a kept-alive component's child components may // change, so directly walking the tree here may call activated hooks // on incorrect children. Instead we push them into a queue which will // be processed after the whole patch process ended. queueActivatedComponent(componentInstance) } else { activateChildComponent(componentInstance, true /* direct */) } } }, destroy (vnode: MountedComponentVNode) { const { componentInstance } = vnode if (!componentInstance._isDestroyed) { if (!vnode.data.keepAlive) { componentInstance.$destroy() } else { deactivateChildComponent(componentInstance, true /* direct */) } } } }
代码 1.10
通过代码 1.9 和 代码 1.10
中可以了解到组件创建过程中就会把各种钩子函数合并并按顺序执行。这里要注意的是合并策略,在合并过程中,如果某个时机的钩子已经存在 data.hook
中,那么通过执行 mergeHook
函数做合并,这个逻辑很简单,就是在最终执行的时候,依次执行这两个钩子函数即可
前面啰里啰嗦一大堆,其实代码最关键的地方往往就是那么几行可执行的方式
const name = Ctor.options.name || tag const vnode = new VNode( `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`, data, undefined, undefined, undefined, context, { Ctor, propsData, listeners, tag, children }, asyncFactory ) return vnode
代码 1.11
虽然简单,但是关键就在此处。通过 new VNode
实例化一个vnode
并返回。需要注意的和普通元素节点不同vnode
,组件的vnode
没有children
的,但是第六个参数对象中含有,具体细节后续分析。
其实最后就是组件初始化过程中先要合并参数,确保数据流单向流向,处理一些参数异常问题,合并钩子函数,最后初始化节点返回vnode
您的鼓励是我前进的动力---
使用微信扫描二维码完成支付