阅读 328

组件渲染成 VNode(产品渲染图是怎么做的)

在《render 渲染原理》一文中,分析了 render 函数如何将 Vue 实例渲染成 VNode。在这渲染过程中,有两种方式:一是普通节点渲染;二是组件渲染。而当时只分析了普通节点的渲染过程,那么本文将分析组件的渲染过程。

组件渲染成 VNode

按照惯例,沿着主线将组件渲染成 VNode 的过程整理成一张图,如下:

component-render-vnode.png

通过例子来进行分析:

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 钩子函数 initprepatchinsertdestroy 合并到 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/ 


文章分类
代码人生
文章标签
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐