手撕Vue源码——createComponent

组件化

Vue.js另一个核心思想就是组件化。所谓组件化,就是把页面拆成多个组件(component),每个组件依赖的css,JavaScript,模板,图片等资源放在一起开发和维护。组件是资源独立的,组件在系统内部可服用,组件和组件之间可以嵌套。

我们在Vue.js开发实际项目的时候,就是像搭积木一样,编写一堆组件拼装生成页面。Vue.js的官网中,也是花了大篇幅来介绍什么事组件,如何编写组件以及组件拥有的属性和特性。

那么我们将从源码的角都来分析Vue的组件内部是如何工作的,只有了解了内部的工作原理,才能让我们使用它的时候更加得心应手。

接下来我们会用Vue-cli初始化的代码为例,来分析一下Vue的初始化的一个过程。

import Vue from 'vue'
import App from './App.vue'

var app = new Vue({
	el: "#app",
	// 这里的h是createElement方法
	render: h => h(App)
})
复制代码

这段代码相信很多同学都很熟悉,他和我们之前说的render函数有一定的相同点,不同的这次通过createElement穿的参数是一个组件而不是一个原生的标签,那么接下来我们就开始分析这一过程

createElement

createElement的实现的时候,他最终会调用_ createElement 方法,其中有一段逻辑是对参数tag的判断,如果是一个普通的html标签,如果是一个普通的div,则会实例化一个普通的VNode,否则通过createComponent方法创建一个组件VNode。

  if (typeof tag === 'string') {
    let Ctor
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    if (config.isReservedTag(tag)) {
      // platform built-in elements
      if (process.env.NODE_ENV !== 'production' && isDef(data) && isDef(data.nativeOn) && data.tag !== 'component') {
        warn(
          `The .native modifier for v-on is only valid on components but it was used on <${tag}>.`,
          context
        )
      }
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )
    } else if ((!data || !data.pre) && 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)
  }
复制代码

我们传入的是一个App对象,它本质上是一个Component类型,那么他会走到上述代码的else逻辑,直接通过createComponent方法来创建vnode。所以接下来我们来看一下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 (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
}
复制代码

可以看到,createComponent的逻辑也会有一些复杂,但是分析源码比较推荐的是值分析核心流程,分支流程可以之后针对性的看,所以这里针对组件渲染这个case主要就3个关键步骤:

构造子类构造函数,安装组件钩子函数和实例化vnode。

构造子类构造函数

const baseCtor = context.$options._ base
//plain options object: turn it into a constructor
if (isObject(Ctor)) {
    Ctor = baseCtor.extend(Ctor)
}
复制代码

我们在编写一个组件的时候,通常都是创建一个普通对象,还是以我们的App.vue为例,代码如下:

import HelloWorld form "./component/HelloWorld"

export default {
    name: 'app',
    components: {
        HelloWorld
    }
}
复制代码

这里export的是一个对象,所以createComponent里的代码逻辑会执行到baseCtor,extend(Ctor),在这里baseCtor实际上就是Vue,这个的定义实在最开始初始化Vue的节点,在src/core/global-api/index.js 中的initGlobalAPI函数有这么一段逻辑

Vue.options._ base = Vue
复制代码

细心的同学会发现,这里定义的是Vue.option,而我们的createComponent取得是context.$options,实际上src/core/instance/init.js里Vue圆形上的 _ init 函数中有这么一段逻辑

vm.$options = mergeOptions(
  resolveConstructorOptions(vm.constructor),
  options || {},
  vm
)
复制代码

这样吧Vue上的一些option扩展到了vm.option上,所以我们也就能通过vm.option上,所以我们也就能通过vm.options._ base拿到Vue这个构造函数了。mergeOptions的实现我们会在之后来看,现在我们只需要理解他的功能是吧Vue构造函数的option和用户传入的option做一层合并,到vm.$options上。在了解了baseCtor指向了Vue之后,我们来看一下Vue.extend函数的定义,在src/core/global-api/extend.js中

  /**
   * 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' && 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
  }
复制代码

Vue.extend的作用就是构造一个Vue的子类,他是用一种非常经典的原型集成的方式吧一个纯对象转换成一个继承与Vue的构造器Sub并返回,然后对Sub这个对象本身扩展了一些属性,如扩展options、添加全局API等;并且对配置中的props和Computed做了初始化工作;最后对于这个Sub构造函数做了缓存,避免多次执行Vue.extend的时候对同一个子组件重复构造。

这样当我们去实例化Sub的时候,就会执行this. _ init逻辑再次走到了Vue实例的初始化逻辑,实例化子组件的逻辑在之后回进行介绍。

const Sub = function VueComponent (options) {
    this._init(options)
}
复制代码

安装组件钩子函数

installComponentHooks(data)
复制代码

我们之前提到了Vue.js使用VirtualDOM参考的是开源库snabbdom,他的一个特点是在VNode的patch流程中对外暴露了各种时机的钩子函数,方便我们做一些额外的事情,Vue.js也是充分利用这一点,在初始化一个Component类型的VNode的过程中实现了几个钩子函数:

// 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
    )
  },

  const hooksToMerge = Object.keys(componentVNodeHooks)
      
  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 */)
      }
    }
  }
}
复制代码

整个installComponentHooks 的过程就是把componentVNodeHooks的钩子函数合并到data.hook中,在VNode执行patch的过程中执行相关的钩子函数,在合并过程中,如果某个时机的钩子已经存在data.hook中,那么通过执行mergeHook函数做合并,这个逻辑很简单,就是在最终执行的时候,依赖执行这两个钩子函数既可。

实例化VNode

  // 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
  )
复制代码

最后一步非常简单,通过new VNode 实例化一个vnode并返回。需要注意的是和普通元素节点的vnode不同,组件的vnode是没有children的,这点很关键。

总结

这一节我们分析了createComponent的实现,了解到他在渲染一个组件的会后的三个关键逻辑:构造子类构造函数,安装组件钩子函数和实例化vnode。createComponent后返回的是组件vnode,他也一样走到vm._ update方法,进而执行了pattch函数。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享