组件渲染成 VNode(产品渲染图是怎么做的)
在《render 渲染原理》一文中,分析了 render
函数如何将 Vue 实例渲染成 VNode。在这渲染过程中,有两种方式:一是普通节点渲染;二是组件渲染。而当时只分析了普通节点的渲染过程,那么本文将分析组件的渲染过程。
组件渲染成 VNode
按照惯例,沿着主线将组件渲染成 VNode 的过程整理成一张图,如下:
通过例子来进行分析:
import Vue from 'vue' import App from './App.vue' Vue.config.productionTip = false new Vue({ render: h => h(App), }).$mount('#app') 复制代码
还记得在函数 _createElement
有这么一段逻辑:
if (typeof tag === 'string') { ... } else { // direct component options / constructor vnode = createComponent(tag, data, context, children) } 复制代码
通过例子可以知道,用户手写 render
函数渲染 App
组件;因此,tag
其数据类型不是 string
,而是一个对象(组件对象),从而进入 else
分支,调用函数 createComponent
生成组件 VNode。
createComponent 内部实现
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) } // 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 } } // install component management hooks onto the placeholder node installComponentHooks(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 ) // 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 } 复制代码
函数接收 5 个参数,分别如下:
Ctor
:表示一个组件对象data
:表示 VNode 数据context
:表示 Vue 实例children
:表示 VNode 子节点tag
:表示标签
作用:将组件渲染成组件 VNode。
从函数的代码实现来看下,其功能点主要有:
构造子类构造函数
异步组件逻辑
解析&合并 options
v-model
逻辑提取
props
函数式组件逻辑
安装组件钩子函数
实例化组件 VNode
而在本文中,着重分析加粗要点。
构造子类构造函数
const baseCtor = context.$options._base // plain options object: turn it into a constructor if (isObject(Ctor)) { Ctor = baseCtor.extend(Ctor) } 复制代码
对于 context.$options._base
,有没有好奇它是怎么来的?下面一步一步地来揭开其面纱。
在初始化 Vue 时,位于 src/core/index.js
文件里,有这么一行代码:
initGlobalAPI(Vue) 复制代码
而在该函数的实现中,也有这么一行代码
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 } 复制代码
可是你会发现 _base
是定义在 Vue.options
,而使用的是 Vue.$options._base
,那么这其中又发生了什么呢?
其实在实例化 Vue 的过程中,有一段逻辑是对 options
进行合并处理的:
vm.$options = mergeOptions( resolveConstructorOptions(vm.constructor), options || {}, vm ) 复制代码
将用户传入的 options
与 Vue 构造函数的 options
进行合并,最后将合并后的 options
赋值给 vm.$options
,因此可以通过 vm.$options
访问到 _base
,从而可知 _base
实际是指向 Vue。
在这里,Ctor
是指 App
组件对象,isObject(Ctor)
为 true
,则进入 if
分支逻辑,调用 Vue 方法 extend
构建子类构造涵,即
baseCtor.extend(Ctor)
,那么来看下其是如何实现的?(src/core/global-api/extend.js
)
/** * Class inheritance */ Vue.extend = function (extendOptions: Object): Function { extendOptions = extendOptions || {} const Super = this // 指向 Vue 构造函数,而不是 Vue 实例 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) } 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 } 复制代码
extend
接收一个参数:
extendOptions
:其数据类型是一个Object
,指向export default {}
,即平时编写 Vue 组件时导出的对象
作用:构造一个 Vue
的子类,采用一种非常经典的原型继承方式将一个纯对象转换成一个继承于 Vue
构造器的子类 Sub
;同时对其进行扩展,并且将 Sub
构造函数进行缓存,避免对同一个组件进行重复构造,多次执行 Vue.extend
。
const SuperId = Super.cid const cachedCtors = extendOptions._Ctor || (extendOptions._Ctor = {}) if (cachedCtors[SuperId]) { return cachedCtors[SuperId] } 复制代码
变量 cachedCtors
用来缓存组件(Sub
)构造函数,cid
作为 key
,而 Sub
构造函数作为 value
。如果同一个组件被多次加载,那么除了第一次会执行 Vue.extend
外,其它都是直接从缓存中获取组件构造函数的。
const name = extendOptions.name || Super.options.name if (process.env.NODE_ENV !== 'production' && name) { validateComponentName(name) } 复制代码
获取组件名称,并对其名称进行合法性校验。如果子组件没有定义 name
属性,则直接从其父组件获取 name
属性。
const Sub = function VueComponent (options) { this._init(options) } Sub.prototype = Object.create(Super.prototype) Sub.prototype.constructor = Sub 复制代码
这里是采用经典的原型继承方式来构造子类构造函数,即将一个纯对象转换成一个继承于 Vue 构造函数的子类 Sub
。也就是说,先声明构造函数 Sub
,并且将 Sub
原型 prototype
指向 Vue 原型,通过 Object.create
实现;同时将 Sub.prototype.constructor
指向其自身 Sub
,从而实现了继承。
Sub.options = mergeOptions( Super.options, extendOptions ) 复制代码
将构造函数 Super
自带的 options
与 用户传入的 options
进行合并,并将其合并结果赋值给 Sub.options
。
// 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) } 复制代码
如果 Sub.options
包含 props
属性,则对其进行初始化;如果 Sub.options
包含 computed
属性,则对其进行初始化。
// 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] }) 复制代码
将 Super
属性赋值给 Sub
构造函数,对其进行扩展。
// cache constructor cachedCtors[SuperId] = Sub 复制代码
通过对象 cachedCtors
来缓存 Sub
构造函数,返回 Sub
。
安装组件钩子函数
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 } } } 复制代码
函数 installComponentHooks
接收一个参数:
data
:其数据类型是VNodeData
,表示 VNode data
作用:将 componentVNodeHooks
钩子函数 init
、prepatch
、insert
、destroy
合并到 hooks
中;在合并过程中,如果 hooks
存在相应的钩子函数,则调用 mergeHook
函数对其进行合并,采取的合并策略是依次执行钩子函数;否则直接保存到 hooks
中。最终在 VNode pathc
过程中执行对应的钩子函数。
那么来看下四个钩子函数具体是如何实现的?
// inline hooks to be invoked on component VNodes during patch 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 */) } } } } 复制代码
对于 hooks
已经存在的钩子函数,采用的合并策略是调用 mergeHook
,具体实现如下:
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 } 复制代码
实例化组件 VNode
const vnode = new VNode( `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`, data, undefined, undefined, undefined, context, { Ctor, propsData, listeners, tag, children }, asyncFactory ) 复制代码
最终是通过 new VNode
来实例化组件 VNode,并将其最终结果返回。
至此,组件渲染成 VNode 的核心逻辑已经分析完了。
作者:PANJU
链接:https://juejin.cn/post/7038227186930876447
伪原创工具 SEO网站优化 https://www.237it.com/