vue组件化过程——Vue源码解析

component的过程很有意思的过程

不错鼓励并赞赏 标签: Javascript      评论 / 2019-09-15

组件化component的创建过程——Vue源码解析

大家都知道,使用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 = Vuesrc/core/global-api/index.jsinitGlobalAPI函数中体现。

还有关于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 函数做合并,这个逻辑很简单,就是在最终执行的时候,依次执行这两个钩子函数即可

  • 实例化为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.11

虽然简单,但是关键就在此处。通过 new VNode实例化一个vnode并返回。需要注意的和普通元素节点不同vnode,组件的vnode没有children的,但是第六个参数对象中含有,具体细节后续分析。

总结

其实最后就是组件初始化过程中先要合并参数,确保数据流单向流向,处理一些参数异常问题,合并钩子函数,最后初始化节点返回vnode

Hi 看这里!

大家好,我是PRO

我会陆续分享生活中的点点滴滴,当然不局限于技术。希望笔墨之中产生共鸣,每篇文章下面可以留言互动讨论。Tks bd!

博客分类

您可能感兴趣

作者推荐

呃,突然想说点啥

前端·博客

您的鼓励是我前进的动力---

使用微信扫描二维码完成支付