引言
Vue.js 中的一个核心思想是组件化。所谓组件化,就是把页面拆分成多个组件 (component),每个组件依赖的 CSS、JavaScript、模板、图片等资源放在一起开发和维护。组件化思想允许我们使用小型、独立和通常可复用的组件构建大型应用。几乎任意类型的应用界面都可以抽象为一个组件树,这里参考官网的一张图来说明:
接下来的几篇文章,我会带大家一起来看下组件化相关的源码,了解这块有助于我们了解组件化的思想。
本小节我们先来看下createComponent函数的实现。
从一个简单示例开始回顾Vue源码探秘(五)(_render 函数的实现),我们是这么编写render函数的:
new Vue({ // 这里的 h 是 createElement 方法 render: function(h) { return h( "div", { attrs: { id: "app" } }, this.message ); } });而如果使用单文件组件,我们需要这样编写render函数:
import Vue from "vue"; import App from "./App.vue"; var app = new Vue({ el: "#app", // 这里的 h 是 createElement 方法 render: h => h(App) });上面两种编写方式有什么不同呢?很显然两种编写render函数的方式都是通过 render 函数去渲染的,不同的是这次通过 createElement 传的参数是一个组件而不是一个原生的标签。下面我们就结合上面这个例子开始分析。
createComponent回顾Vue源码探秘(七)(createElement),我们在分析_createElement函数时,有这么一段代码:
// src/core/vdom/create-element.js export function _createElement(): VNode | Array<VNode> { // ... // ... if (typeof tag === "string") { // ... } else { // direct component options / constructor vnode = createComponent(tag, data, context, children); } // ... }这里对参数 tag 进行了判断,如果是一个普通的 html 标签,像上一章的例子那样是一个普通的 div,则会实例化一个普通 VNode 节点,否则通过 createComponent 方法创建一个组件 VNode。createComponent 函数定义在 src/core/vdom/create-component.js 中,我们分段来分析:
// src/core/vdom/create-component.js export 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; } const baseCtor = context.$options._base; // plain options object: turn it into a constructor if (isObject(Ctor)) { Ctor = baseCtor.extend(Ctor); } // ... }函数一开始将 vm.$option 的 _base 属性赋给 baseCtor。在这里 baseCtor 实际上就是 Vue,这个的定义是在最开始初始化 Vue 的阶段,在 src/core/global-api/index.js 中的 initGlobalAPI 函数有这么一段逻辑:
// src/core/global-api/index.js export function initGlobalAPI(Vue: GlobalAPI) { // ... // this is used to identify the "base" constructor to extend all plain-object // components with in Weex's multi-instance scenarios. Vue.options._base = Vue; // ... }可以看到这里定义的是Vue.options,而我们在createComponent中取的是context.$options。这块其实是在src/core/instance/init.js里 Vue 原型上的 _init 方法中处理的:
// src/core/instance/init.js Vue.prototype._init = function(options?: Object) { vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm ); };这里调用了mergeOptions函数,将Vue.options合并到vm.$options上,因此这里就可以通过vm.$options._base 拿到 Vue 构造函数。
回到createComponent函数,接下来判断Ctor是不是对象。这里的Ctor是指什么呢?先来看一下我们平时经常编写的单文件组件:
<template> // ... </template> <script> export default { name: 'App' } </script>Ctor 就是单文件组件导出的对象。这里调用了 Vue.extend(Ctor)。
Vue.extend( options )使用基础 Vue 构造器,创建一个“子类”。参数是一个包含组件选项的对象。具体参考https://cn.vuejs.org/v2/api/#Vue-extend
extend 定义在 src/core/global-api/extend.js 文件中,我们分段来分析它的实现原理:
// src/core/global-api/extend.js 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" && name) { validateComponentName(name); } // ... };extend 函数首先做了一些初始化工作,这里定义的 cachedCtors 的具体作用在下文会介绍。然后调用 validateComponentName 函数对 extendOptions 的 name 属性(也就是组件名)进行校验。validateComponentName 函数代码如下:
// src/core/util/options.js export function validateComponentName(name: string) { if ( !new RegExp(`^[a-zA-Z][\-\.0-9_${unicodeRegExp.source}]*$`).test(name) ) { warn( 'Invalid component name: "' + name + '". Component names ' + "should conform to valid custom element name in html5 specification." ); } if (isBuiltInTag(name) || config.isReservedTag(name)) { warn( "Do not use built-in or reserved HTML elements as component " + "id: " + name ); } }第一个if语句是检查组件名是否符合 HTML5 自定义元素的命名规范。第二个if语句检查组件名是否和内置 HTML 元素命名冲突。回到 extend 函数,继续往下看:
// src/core/global-api/extend.js Vue.extend = function(extendOptions: Object): Function { // ... 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]; }); };这一段代码定义了子类的构造函数 Sub,然后对 Sub 这个对象本身扩展了一些属性,如扩展 options、添加全局 API 等;并且对配置中的 props 和 computed 做了初始化工作。
继续看 extend 函数最后一段代码:
// src/core/global-api/extend.js Vue.extend = function(extendOptions: Object): Function { // ... // 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; };这里其实就是将创建好的构造函数 Sub 保存到组件的属性中作缓存,如果这个组件被其他组件多次引用,那么这个组件会多次作为参数传给 extend 函数,这样检查到之前的缓存就可以直接将 Sub 返回而不用重新构造了。
这样也就解释了上面提到的cachedCtors的作用了。
分析完 extend 函数,我们回到 createComponent 函数,接着往下看:
// src/core/vdom/create-component.js export function createComponent( Ctor: Class<Component> | Function | Object | void, data: ?VNodeData, context: Component, children: ?Array<VNode>, tag?: string ): VNode | Array<VNode> | void { // ... // 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); 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; } } // ... }这里首先判断如果Ctor不是函数则抛出警告并结束函数,接下来的一段是与异步组件相关,异步组件相关的我会在后面单独出一节来分析。
然后对data做初始化处理,并调用 resolveConstructorOptions 解析构造函数 Ctor 的 options。
接下来这一段涉及到了 v-model 指令,和异步组件一样,也会在后面单独出一节介绍 v-model 。
后面的代码和 props、函数式组件、监听器相关,这里都先略过。继续往下:
// src/core/vdom/create-component.js export function createComponent( Ctor: Class<Component> | Function | Object | void, data: ?VNodeData, context: Component, children: ?Array<VNode>, tag?: string ): VNode | Array<VNode> | void { // ... // install component management hooks onto the placeholder node installComponentHooks(data); // ... }这一步是调用 installComponentHooks 函数来安装组件钩子函数,来看 installComponentHooks 函数的代码:
// src/core/vdom/create-component.js function installComponentHooks(data: VNodeData) { const hooks = data.hook || (data.hook = {}); 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; } } }这里的hooksToMerge和componentVNodeHooks是什么呢?来看下它们的定义:
// src/core/vdom/create-component.js 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 { const child = (vnode.componentInstance = createComponentInstanceForVnode( vnode, activeInstance )); child.$mount(hydrating ? vnode.elm : undefined, hydrating); } }, 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 */); } } } }; const hooksToMerge = Object.keys(componentVNodeHooks);可以看到,componentVNodeHooks 定义了四个钩子函数。
我们之前提到 Vue.js 使用的 Virtual DOM 参考的是开源库 snabbdom,它的一个特点是在 VNode 的 patch 流程中对外暴露了各种时机的钩子函数,方便我们做一些额外的事情。
整个 installComponentHooks 的过程就是把 componentVNodeHooks 的钩子函数合并到 data.hook 中,在 VNode 执行 patch 的过程中执行相关的钩子函数。
这里要注意一下合并策略mergeHook,看下代码:
// src/core/vdom/create-component.js function mergeHook(f1: any, f2: any): Function { const merged = (a, b) => { // flow complains about extra args which is why we use any f1(a, b); f2(a, b); }; merged._merged = true; return merged; }mergeHook 函数逻辑很简单,所谓合并就是先执行 componentVNodeHooks 定义的再执行 data.hooks 定义的,再将合并标志位设为 true。
createComponent 函数还剩最后一段代码:
// src/core/vdom/create-component.js export function createComponent( Ctor: Class<Component> | Function | Object | void, data: ?VNodeData, context: Component, children: ?Array<VNode>, tag?: string ): VNode | Array<VNode> | void { // ... // 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 ); // Weex specific: invoke recycle-list optimized @render function for // extracting cell-slot template. // https://github.com/Hanks10100/weex-native-directive/tree/master/component /* istanbul ignore if */ if (__WEEX__ && isRecyclableComponent(vnode)) { return renderRecyclableComponentTemplate(vnode); } return vnode; }最后这一段的逻辑是生成一个 VNode 并返回。这里需要注意的是由于组件 VNode 是没有 children 的,所以这里 new VNode 的第三个参数 children 是 undefined。
总结这一节我们分析了 createComponent 函数的执行流程,它有三个关键的步骤:
构建子类构造函数安装组件钩子函数创建VNode并返回createComponent 后返回的是组件 vnode,它也一样走到 vm._update 方法,进而执行了 patch 函数。我们已经研究过针对普通 VNode 节点的情况了,下一节我们将研究 __patch__ 怎么把组件的 VNode 转换成真实 DOM。
---来自腾讯云社区的---前端森林
微信扫一扫打赏
支付宝扫一扫打赏